@carlonicora/nestjs-neo4jsonapi 4.4.0 → 4.6.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 (215) hide show
  1. package/dist/agents/responder/factories/responder.context.factory.d.ts +2 -1
  2. package/dist/agents/responder/factories/responder.context.factory.d.ts.map +1 -1
  3. package/dist/agents/responder/factories/responder.context.factory.js.map +1 -1
  4. package/dist/agents/responder/services/responder.service.d.ts +16 -1
  5. package/dist/agents/responder/services/responder.service.d.ts.map +1 -1
  6. package/dist/agents/responder/services/responder.service.js +14 -2
  7. package/dist/agents/responder/services/responder.service.js.map +1 -1
  8. package/dist/bootstrap/app.module.factory.d.ts.map +1 -1
  9. package/dist/bootstrap/app.module.factory.js +2 -0
  10. package/dist/bootstrap/app.module.factory.js.map +1 -1
  11. package/dist/bootstrap/bootstrap.options.d.ts +35 -0
  12. package/dist/bootstrap/bootstrap.options.d.ts.map +1 -1
  13. package/dist/common/index.d.ts +1 -0
  14. package/dist/common/index.d.ts.map +1 -1
  15. package/dist/common/index.js +4 -0
  16. package/dist/common/index.js.map +1 -1
  17. package/dist/common/repositories/handbook.ai-source-query.provider.d.ts +34 -0
  18. package/dist/common/repositories/handbook.ai-source-query.provider.d.ts.map +1 -0
  19. package/dist/common/repositories/handbook.ai-source-query.provider.js +58 -0
  20. package/dist/common/repositories/handbook.ai-source-query.provider.js.map +1 -0
  21. package/dist/common/repositories/index.d.ts +1 -0
  22. package/dist/common/repositories/index.d.ts.map +1 -1
  23. package/dist/common/repositories/index.js +1 -0
  24. package/dist/common/repositories/index.js.map +1 -1
  25. package/dist/common/types/data.limits.d.ts +4 -0
  26. package/dist/common/types/data.limits.d.ts.map +1 -1
  27. package/dist/common/utils/chunk-content.util.d.ts +28 -0
  28. package/dist/common/utils/chunk-content.util.d.ts.map +1 -0
  29. package/dist/common/utils/chunk-content.util.js +32 -0
  30. package/dist/common/utils/chunk-content.util.js.map +1 -0
  31. package/dist/config/base.config.d.ts.map +1 -1
  32. package/dist/config/base.config.js +1 -0
  33. package/dist/config/base.config.js.map +1 -1
  34. package/dist/config/enums/queue.id.d.ts +1 -0
  35. package/dist/config/enums/queue.id.d.ts.map +1 -1
  36. package/dist/config/enums/queue.id.js +1 -0
  37. package/dist/config/enums/queue.id.js.map +1 -1
  38. package/dist/config/interfaces/config.two.factor.interface.d.ts +10 -3
  39. package/dist/config/interfaces/config.two.factor.interface.d.ts.map +1 -1
  40. package/dist/core/core.module.d.ts.map +1 -1
  41. package/dist/core/core.module.js +8 -1
  42. package/dist/core/core.module.js.map +1 -1
  43. package/dist/core/llm/services/model.service.d.ts +13 -0
  44. package/dist/core/llm/services/model.service.d.ts.map +1 -1
  45. package/dist/core/llm/services/model.service.js +22 -0
  46. package/dist/core/llm/services/model.service.js.map +1 -1
  47. package/dist/core/neo4j/abstracts/abstract.service.d.ts.map +1 -1
  48. package/dist/core/neo4j/abstracts/abstract.service.js +8 -1
  49. package/dist/core/neo4j/abstracts/abstract.service.js.map +1 -1
  50. package/dist/foundations/assistant/controllers/assistant.controller.d.ts.map +1 -1
  51. package/dist/foundations/assistant/controllers/assistant.controller.js +6 -2
  52. package/dist/foundations/assistant/controllers/assistant.controller.js.map +1 -1
  53. package/dist/foundations/assistant/dtos/assistant-append.dto.d.ts +2 -0
  54. package/dist/foundations/assistant/dtos/assistant-append.dto.d.ts.map +1 -1
  55. package/dist/foundations/assistant/dtos/assistant-append.dto.js +10 -0
  56. package/dist/foundations/assistant/dtos/assistant-append.dto.js.map +1 -1
  57. package/dist/foundations/assistant/dtos/assistant-post.dto.d.ts +2 -0
  58. package/dist/foundations/assistant/dtos/assistant-post.dto.d.ts.map +1 -1
  59. package/dist/foundations/assistant/dtos/assistant-post.dto.js +10 -0
  60. package/dist/foundations/assistant/dtos/assistant-post.dto.js.map +1 -1
  61. package/dist/foundations/assistant/services/assistant.service.d.ts +8 -1
  62. package/dist/foundations/assistant/services/assistant.service.d.ts.map +1 -1
  63. package/dist/foundations/assistant/services/assistant.service.js +14 -1
  64. package/dist/foundations/assistant/services/assistant.service.js.map +1 -1
  65. package/dist/foundations/foundations.modules.d.ts +6 -0
  66. package/dist/foundations/foundations.modules.d.ts.map +1 -1
  67. package/dist/foundations/foundations.modules.js +10 -2
  68. package/dist/foundations/foundations.modules.js.map +1 -1
  69. package/dist/foundations/handbook/controllers/handbook-thread.controller.d.ts +61 -0
  70. package/dist/foundations/handbook/controllers/handbook-thread.controller.d.ts.map +1 -0
  71. package/dist/foundations/handbook/controllers/handbook-thread.controller.js +170 -0
  72. package/dist/foundations/handbook/controllers/handbook-thread.controller.js.map +1 -0
  73. package/dist/foundations/handbook/controllers/handbook.controller.d.ts +29 -0
  74. package/dist/foundations/handbook/controllers/handbook.controller.d.ts.map +1 -0
  75. package/dist/foundations/handbook/controllers/handbook.controller.js +123 -0
  76. package/dist/foundations/handbook/controllers/handbook.controller.js.map +1 -0
  77. package/dist/foundations/handbook/dtos/handbook-page.dto.d.ts +11 -0
  78. package/dist/foundations/handbook/dtos/handbook-page.dto.d.ts.map +1 -0
  79. package/dist/foundations/handbook/dtos/handbook-page.dto.js +45 -0
  80. package/dist/foundations/handbook/dtos/handbook-page.dto.js.map +1 -0
  81. package/dist/foundations/handbook/dtos/handbook-thread-message-post.dto.d.ts +24 -0
  82. package/dist/foundations/handbook/dtos/handbook-thread-message-post.dto.d.ts.map +1 -0
  83. package/dist/foundations/handbook/dtos/handbook-thread-message-post.dto.js +60 -0
  84. package/dist/foundations/handbook/dtos/handbook-thread-message-post.dto.js.map +1 -0
  85. package/dist/foundations/handbook/dtos/handbook-thread-patch.dto.d.ts +19 -0
  86. package/dist/foundations/handbook/dtos/handbook-thread-patch.dto.d.ts.map +1 -0
  87. package/dist/foundations/handbook/dtos/handbook-thread-patch.dto.js +59 -0
  88. package/dist/foundations/handbook/dtos/handbook-thread-patch.dto.js.map +1 -0
  89. package/dist/foundations/handbook/dtos/handbook-thread-post.dto.d.ts +24 -0
  90. package/dist/foundations/handbook/dtos/handbook-thread-post.dto.d.ts.map +1 -0
  91. package/dist/foundations/handbook/dtos/handbook-thread-post.dto.js +60 -0
  92. package/dist/foundations/handbook/dtos/handbook-thread-post.dto.js.map +1 -0
  93. package/dist/foundations/handbook/entities/handbook-page.d.ts +25 -0
  94. package/dist/foundations/handbook/entities/handbook-page.d.ts.map +1 -0
  95. package/dist/foundations/handbook/entities/handbook-page.js +45 -0
  96. package/dist/foundations/handbook/entities/handbook-page.js.map +1 -0
  97. package/dist/foundations/handbook/entities/handbook-page.meta.d.ts +3 -0
  98. package/dist/foundations/handbook/entities/handbook-page.meta.d.ts.map +1 -0
  99. package/dist/foundations/handbook/entities/handbook-page.meta.js +10 -0
  100. package/dist/foundations/handbook/entities/handbook-page.meta.js.map +1 -0
  101. package/dist/foundations/handbook/entities/handbook-section.d.ts +22 -0
  102. package/dist/foundations/handbook/entities/handbook-section.d.ts.map +1 -0
  103. package/dist/foundations/handbook/entities/handbook-section.js +24 -0
  104. package/dist/foundations/handbook/entities/handbook-section.js.map +1 -0
  105. package/dist/foundations/handbook/entities/handbook-section.meta.d.ts +3 -0
  106. package/dist/foundations/handbook/entities/handbook-section.meta.d.ts.map +1 -0
  107. package/dist/foundations/handbook/entities/handbook-section.meta.js +10 -0
  108. package/dist/foundations/handbook/entities/handbook-section.meta.js.map +1 -0
  109. package/dist/foundations/handbook/entities/handbook-thread-message.d.ts +36 -0
  110. package/dist/foundations/handbook/entities/handbook-thread-message.d.ts.map +1 -0
  111. package/dist/foundations/handbook/entities/handbook-thread-message.js +28 -0
  112. package/dist/foundations/handbook/entities/handbook-thread-message.js.map +1 -0
  113. package/dist/foundations/handbook/entities/handbook-thread-message.meta.d.ts +3 -0
  114. package/dist/foundations/handbook/entities/handbook-thread-message.meta.d.ts.map +1 -0
  115. package/dist/foundations/handbook/entities/handbook-thread-message.meta.js +10 -0
  116. package/dist/foundations/handbook/entities/handbook-thread-message.meta.js.map +1 -0
  117. package/dist/foundations/handbook/entities/handbook-thread.d.ts +47 -0
  118. package/dist/foundations/handbook/entities/handbook-thread.d.ts.map +1 -0
  119. package/dist/foundations/handbook/entities/handbook-thread.js +35 -0
  120. package/dist/foundations/handbook/entities/handbook-thread.js.map +1 -0
  121. package/dist/foundations/handbook/entities/handbook-thread.meta.d.ts +3 -0
  122. package/dist/foundations/handbook/entities/handbook-thread.meta.d.ts.map +1 -0
  123. package/dist/foundations/handbook/entities/handbook-thread.meta.js +10 -0
  124. package/dist/foundations/handbook/entities/handbook-thread.meta.js.map +1 -0
  125. package/dist/foundations/handbook/handbook.module.d.ts +21 -0
  126. package/dist/foundations/handbook/handbook.module.d.ts.map +1 -0
  127. package/dist/foundations/handbook/handbook.module.js +135 -0
  128. package/dist/foundations/handbook/handbook.module.js.map +1 -0
  129. package/dist/foundations/handbook/index.d.ts +23 -0
  130. package/dist/foundations/handbook/index.d.ts.map +1 -0
  131. package/dist/foundations/handbook/index.js +39 -0
  132. package/dist/foundations/handbook/index.js.map +1 -0
  133. package/dist/foundations/handbook/interfaces/handbook.config.interface.d.ts +26 -0
  134. package/dist/foundations/handbook/interfaces/handbook.config.interface.d.ts.map +1 -0
  135. package/dist/foundations/handbook/interfaces/handbook.config.interface.js +10 -0
  136. package/dist/foundations/handbook/interfaces/handbook.config.interface.js.map +1 -0
  137. package/dist/foundations/handbook/processors/handbook.processor.d.ts +22 -0
  138. package/dist/foundations/handbook/processors/handbook.processor.d.ts.map +1 -0
  139. package/dist/foundations/handbook/processors/handbook.processor.js +108 -0
  140. package/dist/foundations/handbook/processors/handbook.processor.js.map +1 -0
  141. package/dist/foundations/handbook/repositories/handbook-page.repository.d.ts +135 -0
  142. package/dist/foundations/handbook/repositories/handbook-page.repository.d.ts.map +1 -0
  143. package/dist/foundations/handbook/repositories/handbook-page.repository.js +245 -0
  144. package/dist/foundations/handbook/repositories/handbook-page.repository.js.map +1 -0
  145. package/dist/foundations/handbook/repositories/handbook-section.repository.d.ts +36 -0
  146. package/dist/foundations/handbook/repositories/handbook-section.repository.d.ts.map +1 -0
  147. package/dist/foundations/handbook/repositories/handbook-section.repository.js +91 -0
  148. package/dist/foundations/handbook/repositories/handbook-section.repository.js.map +1 -0
  149. package/dist/foundations/handbook/repositories/handbook-thread-message.repository.d.ts +36 -0
  150. package/dist/foundations/handbook/repositories/handbook-thread-message.repository.d.ts.map +1 -0
  151. package/dist/foundations/handbook/repositories/handbook-thread-message.repository.js +63 -0
  152. package/dist/foundations/handbook/repositories/handbook-thread-message.repository.js.map +1 -0
  153. package/dist/foundations/handbook/repositories/handbook-thread.repository.d.ts +63 -0
  154. package/dist/foundations/handbook/repositories/handbook-thread.repository.d.ts.map +1 -0
  155. package/dist/foundations/handbook/repositories/handbook-thread.repository.js +86 -0
  156. package/dist/foundations/handbook/repositories/handbook-thread.repository.js.map +1 -0
  157. package/dist/foundations/handbook/services/handbook-ingest.service.d.ts +121 -0
  158. package/dist/foundations/handbook/services/handbook-ingest.service.d.ts.map +1 -0
  159. package/dist/foundations/handbook/services/handbook-ingest.service.js +434 -0
  160. package/dist/foundations/handbook/services/handbook-ingest.service.js.map +1 -0
  161. package/dist/foundations/handbook/services/handbook-page.service.d.ts +57 -0
  162. package/dist/foundations/handbook/services/handbook-page.service.d.ts.map +1 -0
  163. package/dist/foundations/handbook/services/handbook-page.service.js +116 -0
  164. package/dist/foundations/handbook/services/handbook-page.service.js.map +1 -0
  165. package/dist/foundations/handbook/services/handbook-section.service.d.ts +15 -0
  166. package/dist/foundations/handbook/services/handbook-section.service.d.ts.map +1 -0
  167. package/dist/foundations/handbook/services/handbook-section.service.js +37 -0
  168. package/dist/foundations/handbook/services/handbook-section.service.js.map +1 -0
  169. package/dist/foundations/handbook/services/handbook-thread-message.service.d.ts +27 -0
  170. package/dist/foundations/handbook/services/handbook-thread-message.service.d.ts.map +1 -0
  171. package/dist/foundations/handbook/services/handbook-thread-message.service.js +39 -0
  172. package/dist/foundations/handbook/services/handbook-thread-message.service.js.map +1 -0
  173. package/dist/foundations/handbook/services/handbook-thread.service.d.ts +164 -0
  174. package/dist/foundations/handbook/services/handbook-thread.service.d.ts.map +1 -0
  175. package/dist/foundations/handbook/services/handbook-thread.service.js +297 -0
  176. package/dist/foundations/handbook/services/handbook-thread.service.js.map +1 -0
  177. package/dist/foundations/how-to/controllers/how-to.controller.d.ts +7 -1
  178. package/dist/foundations/how-to/controllers/how-to.controller.d.ts.map +1 -1
  179. package/dist/foundations/how-to/controllers/how-to.controller.js +18 -12
  180. package/dist/foundations/how-to/controllers/how-to.controller.js.map +1 -1
  181. package/dist/foundations/how-to/controllers/how-to.public.controller.d.ts +7 -0
  182. package/dist/foundations/how-to/controllers/how-to.public.controller.d.ts.map +1 -1
  183. package/dist/foundations/how-to/controllers/how-to.public.controller.js +7 -0
  184. package/dist/foundations/how-to/controllers/how-to.public.controller.js.map +1 -1
  185. package/dist/foundations/how-to/entities/how-to.d.ts.map +1 -1
  186. package/dist/foundations/how-to/entities/how-to.js +60 -13
  187. package/dist/foundations/how-to/entities/how-to.js.map +1 -1
  188. package/dist/foundations/how-to/how-to.module.d.ts +36 -1
  189. package/dist/foundations/how-to/how-to.module.d.ts.map +1 -1
  190. package/dist/foundations/how-to/how-to.module.js +80 -4
  191. package/dist/foundations/how-to/how-to.module.js.map +1 -1
  192. package/dist/foundations/how-to/index.d.ts +1 -0
  193. package/dist/foundations/how-to/index.d.ts.map +1 -1
  194. package/dist/foundations/how-to/index.js +1 -0
  195. package/dist/foundations/how-to/index.js.map +1 -1
  196. package/dist/foundations/how-to/interfaces/how-to.config.interface.d.ts +30 -0
  197. package/dist/foundations/how-to/interfaces/how-to.config.interface.d.ts.map +1 -0
  198. package/dist/foundations/how-to/interfaces/how-to.config.interface.js +10 -0
  199. package/dist/foundations/how-to/interfaces/how-to.config.interface.js.map +1 -0
  200. package/dist/foundations/how-to/processors/how-to.processor.d.ts +3 -1
  201. package/dist/foundations/how-to/processors/how-to.processor.d.ts.map +1 -1
  202. package/dist/foundations/how-to/processors/how-to.processor.js +8 -1
  203. package/dist/foundations/how-to/processors/how-to.processor.js.map +1 -1
  204. package/dist/foundations/how-to/services/how-to.service.d.ts +4 -0
  205. package/dist/foundations/how-to/services/how-to.service.d.ts.map +1 -1
  206. package/dist/foundations/how-to/services/how-to.service.js +47 -24
  207. package/dist/foundations/how-to/services/how-to.service.js.map +1 -1
  208. package/dist/foundations/index.d.ts +1 -0
  209. package/dist/foundations/index.d.ts.map +1 -1
  210. package/dist/foundations/index.js +1 -0
  211. package/dist/foundations/index.js.map +1 -1
  212. package/dist/foundations/two-factor/services/totp.service.d.ts.map +1 -1
  213. package/dist/foundations/two-factor/services/totp.service.js +2 -1
  214. package/dist/foundations/two-factor/services/totp.service.js.map +1 -1
  215. package/package.json +1 -1
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.HandbookThreadRepository = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const nestjs_cls_1 = require("nestjs-cls");
15
+ const abstract_repository_1 = require("../../../core/neo4j/abstracts/abstract.repository");
16
+ const neo4j_service_1 = require("../../../core/neo4j/services/neo4j.service");
17
+ const security_service_1 = require("../../../core/security/services/security.service");
18
+ const handbook_thread_1 = require("../entities/handbook-thread");
19
+ const handbook_thread_meta_1 = require("../entities/handbook-thread.meta");
20
+ const handbook_thread_message_meta_1 = require("../entities/handbook-thread-message.meta");
21
+ /**
22
+ * HandbookThreadRepository
23
+ *
24
+ * `HandbookThread` is `isCompanyScoped: false`, so `buildDefaultMatch()` emits
25
+ * no company filter at all — the `CREATED_BY` owner edge asserted here is the
26
+ * ENTIRE security boundary. Without this override every administrator would
27
+ * read and mutate every other administrator's threads.
28
+ *
29
+ * `$currentUserId` is auto-injected into queryParams by
30
+ * `Neo4jService.initQuery()` (it reads `clsService.get("userId")`), exactly as
31
+ * `AssistantRepository` relies on.
32
+ */
33
+ let HandbookThreadRepository = class HandbookThreadRepository extends abstract_repository_1.AbstractRepository {
34
+ constructor(neo4j, securityService, clsService) {
35
+ super(neo4j, securityService, clsService);
36
+ this.descriptor = handbook_thread_1.HandbookThreadDescriptor;
37
+ }
38
+ buildUserHasAccess() {
39
+ const { nodeName } = this.descriptor.model;
40
+ return `WITH ${nodeName}
41
+ WHERE EXISTS {
42
+ MATCH (${nodeName})-[:CREATED_BY]->(:User {id: $currentUserId})
43
+ }
44
+ WITH ${nodeName}`;
45
+ }
46
+ /**
47
+ * Bump `updatedAt` so the thread list — ordered by the descriptor default
48
+ * `updatedAt DESC` — surfaces the conversation a user just spoke in. Neither
49
+ * creating a child message nor writing the `HAS_MESSAGE` edge touches the
50
+ * parent node, so without this an active thread would sink below a thread
51
+ * that was merely renamed.
52
+ */
53
+ async touch(params) {
54
+ await this.neo4j.writeOne({
55
+ query: `
56
+ MATCH (${handbook_thread_meta_1.handbookThreadMeta.nodeName}:${handbook_thread_meta_1.handbookThreadMeta.labelName} {id: $id})
57
+ SET ${handbook_thread_meta_1.handbookThreadMeta.nodeName}.updatedAt = datetime()
58
+ `,
59
+ queryParams: { id: params.id },
60
+ });
61
+ }
62
+ /**
63
+ * Delete the thread AND its messages. `DETACH DELETE` on the thread alone
64
+ * would drop the `HAS_MESSAGE` edges and orphan every message node, which
65
+ * — because the messages are only ever reachable through their thread —
66
+ * means they could never be read or deleted again.
67
+ *
68
+ * Mirrors `AssistantRepository.delete`.
69
+ */
70
+ async delete(params) {
71
+ await this.neo4j.writeOne({
72
+ query: `
73
+ MATCH (${handbook_thread_meta_1.handbookThreadMeta.nodeName}:${handbook_thread_meta_1.handbookThreadMeta.labelName} {id: $id})
74
+ OPTIONAL MATCH (${handbook_thread_meta_1.handbookThreadMeta.nodeName})-[:HAS_MESSAGE]->(${handbook_thread_message_meta_1.handbookThreadMessageMeta.nodeName}:${handbook_thread_message_meta_1.handbookThreadMessageMeta.labelName})
75
+ DETACH DELETE ${handbook_thread_meta_1.handbookThreadMeta.nodeName}, ${handbook_thread_message_meta_1.handbookThreadMessageMeta.nodeName}
76
+ `,
77
+ queryParams: { id: params.id },
78
+ });
79
+ }
80
+ };
81
+ exports.HandbookThreadRepository = HandbookThreadRepository;
82
+ exports.HandbookThreadRepository = HandbookThreadRepository = __decorate([
83
+ (0, common_1.Injectable)(),
84
+ __metadata("design:paramtypes", [neo4j_service_1.Neo4jService, security_service_1.SecurityService, nestjs_cls_1.ClsService])
85
+ ], HandbookThreadRepository);
86
+ //# sourceMappingURL=handbook-thread.repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handbook-thread.repository.js","sourceRoot":"","sources":["../../../../src/foundations/handbook/repositories/handbook-thread.repository.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,2CAAwC;AACxC,2FAAuF;AACvF,8EAA0E;AAC1E,uFAAmF;AACnF,iEAAuF;AACvF,2EAAsE;AACtE,2FAAqF;AAErF;;;;;;;;;;;GAWG;AAEI,IAAM,wBAAwB,GAA9B,MAAM,wBAAyB,SAAQ,wCAG7C;IAGC,YAAY,KAAmB,EAAE,eAAgC,EAAE,UAAsB;QACvF,KAAK,CAAC,KAAK,EAAE,eAAe,EAAE,UAAU,CAAC,CAAC;QAHzB,eAAU,GAAG,0CAAwB,CAAC;IAIzD,CAAC;IAES,kBAAkB;QAC1B,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;QAC3C,OAAO,QAAQ,QAAQ;;uBAEJ,QAAQ;;mBAEZ,QAAQ,EAAE,CAAC;IAC5B,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,KAAK,CAAC,MAAsB;QAChC,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;YACxB,KAAK,EAAE;iBACI,yCAAkB,CAAC,QAAQ,IAAI,yCAAkB,CAAC,SAAS;cAC9D,yCAAkB,CAAC,QAAQ;OAClC;YACD,WAAW,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE;SAC/B,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,MAAM,CAAC,MAAsB;QACjC,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;YACxB,KAAK,EAAE;iBACI,yCAAkB,CAAC,QAAQ,IAAI,yCAAkB,CAAC,SAAS;0BAClD,yCAAkB,CAAC,QAAQ,sBAAsB,wDAAyB,CAAC,QAAQ,IAAI,wDAAyB,CAAC,SAAS;wBAC5H,yCAAkB,CAAC,QAAQ,KAAK,wDAAyB,CAAC,QAAQ;OACnF;YACD,WAAW,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE;SAC/B,CAAC,CAAC;IACL,CAAC;CACF,CAAA;AAtDY,4DAAwB;mCAAxB,wBAAwB;IADpC,IAAA,mBAAU,GAAE;qCAOQ,4BAAY,EAAmB,kCAAe,EAAc,uBAAU;GAN9E,wBAAwB,CAsDpC"}
@@ -0,0 +1,121 @@
1
+ import { AppLoggingService } from "../../../core/logging/services/logging.service";
2
+ import { ModelService } from "../../../core/llm/services/model.service";
3
+ import { HandbookModuleConfig } from "../interfaces/handbook.config.interface";
4
+ import { HandbookPageRepository } from "../repositories/handbook-page.repository";
5
+ import { HandbookSectionRepository } from "../repositories/handbook-section.repository";
6
+ import { HandbookPageService } from "./handbook-page.service";
7
+ export type HandbookSyncResult = {
8
+ created: number;
9
+ updated: number;
10
+ unchanged: number;
11
+ deleted: number;
12
+ /** Repo-relative paths whose ingest threw. The walk continues past them. */
13
+ failed: string[];
14
+ };
15
+ export declare class HandbookIngestService {
16
+ private readonly config;
17
+ private readonly handbookPageRepository;
18
+ private readonly handbookSectionRepository;
19
+ private readonly handbookPageService;
20
+ private readonly modelService;
21
+ private readonly logger;
22
+ constructor(config: Required<HandbookModuleConfig>, handbookPageRepository: HandbookPageRepository, handbookSectionRepository: HandbookSectionRepository, handbookPageService: HandbookPageService, modelService: ModelService, logger: AppLoggingService);
23
+ sync(): Promise<HandbookSyncResult>;
24
+ /**
25
+ * Writes the display translation onto the pages the English walk produced.
26
+ *
27
+ * The English tree is the source of truth for WHICH pages exist: a translated
28
+ * file whose path has no English counterpart is counted and skipped, never
29
+ * created. Creating it would put an unindexed page in the manual and — worse,
30
+ * once someone pressed sync again — an English-less page in the retrieval
31
+ * store.
32
+ *
33
+ * Nothing here chunks, embeds or enqueues. `updateDisplay` writes three
34
+ * properties; `content`, `contentHash` and `aiStatus` are untouched, so the
35
+ * index after this pass is byte-for-byte the index before it.
36
+ */
37
+ private syncDisplay;
38
+ private walk;
39
+ /**
40
+ * Glob support is deliberately the smallest thing that serves the config:
41
+ * a literal prefix with a trailing `**`, or an exact path. No dependency,
42
+ * nothing to misread.
43
+ */
44
+ private isExcluded;
45
+ /**
46
+ * Splits a leading YAML frontmatter block off a markdown file and returns its
47
+ * scalar keys.
48
+ *
49
+ * Still not a YAML parser: the block this reads is a handful of `key: value`
50
+ * lines, and a dependency buys nothing but new failure modes on a malformed
51
+ * block. Nested structures and lists are ignored rather than half-parsed.
52
+ */
53
+ private splitFrontmatter;
54
+ /**
55
+ * The tree's own `README.md` index, parsed — or empty maps when it has none.
56
+ *
57
+ * Both roots go through this: the English tree and, when one is configured,
58
+ * the display tree. A display README's link paths are relative to the display
59
+ * root, which IS the English relative path, so the summaries it yields key
60
+ * straight onto the English pages.
61
+ */
62
+ private readIndex;
63
+ private emptyIndex;
64
+ /**
65
+ * Reads the tree's own `README.md` index, when it has one.
66
+ *
67
+ * Two maps come out: section key to its heading title and blurb, and
68
+ * repo-relative path to the one-line description the index gives it. Both are
69
+ * optional by construction — an application with no README gets empty maps,
70
+ * prettified section titles and pages with no summary, and the manual still
71
+ * renders.
72
+ */
73
+ private parseIndex;
74
+ /**
75
+ * Splits a section heading into the key it names and the title to print.
76
+ *
77
+ * Two forms are accepted:
78
+ *
79
+ * - `## 03-backend` — the bare directory key. The title is derived from it,
80
+ * which is all a tree with no authored section names can offer.
81
+ * - `## 03-backend — Backend` — the key, a dash, and an authored title. The
82
+ * title is taken as written.
83
+ *
84
+ * The second form is the only place a section name can be written at all: a
85
+ * section is a DIRECTORY, and a directory has no front matter to carry a
86
+ * title. It is what lets a translated index name its sections in its own
87
+ * language, and what lets an English one write "AI" where derivation would
88
+ * sentence-case `06-ai` into "Ai".
89
+ *
90
+ * The separator must carry whitespace on both sides. Keys contain hyphens
91
+ * (`00-start-here`), so a bare `-` would split the key itself.
92
+ */
93
+ private headingOf;
94
+ /**
95
+ * The paragraph beneath a section heading, rebuilt into one sentence.
96
+ *
97
+ * A README wraps its prose, so the blurb arrives as several lines; joining
98
+ * the contiguous run is the difference between a summary and a sentence cut
99
+ * at "and an index". Collection starts at the first line that is neither
100
+ * blank, nor a list item, nor inside a fenced code block, and stops at the
101
+ * first blank line or list item after that.
102
+ *
103
+ * Fenced blocks are stepped over whole: a section that opens with one — the
104
+ * a360 handbook's own `## Checking the handbook` does — must be summarised by
105
+ * its prose, not by a fence or by the command inside it.
106
+ */
107
+ private blurbOf;
108
+ /**
109
+ * `00-start-here` becomes "Start here". The numeric prefix orders the
110
+ * directories on disk and is noise on screen; a key with no prefix is used
111
+ * as written.
112
+ */
113
+ private sectionTitle;
114
+ /**
115
+ * Frontmatter `title` first, then the first H1, then the filename. The
116
+ * handbook this was built against uses frontmatter and no H1 at all, so the
117
+ * H1 branch exists for directories that are written the other way.
118
+ */
119
+ private titleOf;
120
+ }
121
+ //# sourceMappingURL=handbook-ingest.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handbook-ingest.service.d.ts","sourceRoot":"","sources":["../../../../src/foundations/handbook/services/handbook-ingest.service.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gDAAgD,CAAC;AACnF,OAAO,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AAExE,OAAO,EAAmB,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;AAChG,OAAO,EAAE,sBAAsB,EAAE,MAAM,0CAA0C,CAAC;AAClF,OAAO,EAAE,yBAAyB,EAAE,MAAM,6CAA6C,CAAC;AACxF,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AA4BF,qBACa,qBAAqB;IAEL,OAAO,CAAC,QAAQ,CAAC,MAAM;IAChD,OAAO,CAAC,QAAQ,CAAC,sBAAsB;IACvC,OAAO,CAAC,QAAQ,CAAC,yBAAyB;IAC1C,OAAO,CAAC,QAAQ,CAAC,mBAAmB;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM;gBALmB,MAAM,EAAE,QAAQ,CAAC,oBAAoB,CAAC,EAC/D,sBAAsB,EAAE,sBAAsB,EAC9C,yBAAyB,EAAE,yBAAyB,EACpD,mBAAmB,EAAE,mBAAmB,EACxC,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE,iBAAiB;IAGtC,IAAI,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAsJzC;;;;;;;;;;;;OAYG;YACW,WAAW;IAoCzB,OAAO,CAAC,IAAI;IA+CZ;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAQlB;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB;IAexB;;;;;;;OAOG;IACH,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,UAAU;IAOlB;;;;;;;;OAQG;IACH,OAAO,CAAC,UAAU;IAwBlB;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,SAAS;IASjB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,OAAO;IAwBf;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAMpB;;;;OAIG;IACH,OAAO,CAAC,OAAO;CAShB"}
@@ -0,0 +1,434 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.HandbookIngestService = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const crypto_1 = require("crypto");
18
+ const fs_1 = require("fs");
19
+ const path_1 = require("path");
20
+ const logging_service_1 = require("../../../core/logging/services/logging.service");
21
+ const model_service_1 = require("../../../core/llm/services/model.service");
22
+ const handbook_config_interface_1 = require("../interfaces/handbook.config.interface");
23
+ const handbook_page_repository_1 = require("../repositories/handbook-page.repository");
24
+ const handbook_section_repository_1 = require("../repositories/handbook-section.repository");
25
+ const handbook_page_service_1 = require("./handbook-page.service");
26
+ /**
27
+ * Bumped whenever the walk changes how a file becomes a page — frontmatter
28
+ * handling, title derivation, what is stripped before chunking.
29
+ *
30
+ * It is mixed into `contentHash`, so a parsing change invalidates every stored
31
+ * page and the next sync re-ingests them. Without it the hash only tracks the
32
+ * file's bytes: a fixed parser leaves every already-ingested page frozen in the
33
+ * shape the old parser gave it, and the fix silently does nothing. That is
34
+ * exactly what happened on the 2026-09-11 verification run.
35
+ */
36
+ const INGEST_FORMAT_VERSION = "2";
37
+ let HandbookIngestService = class HandbookIngestService {
38
+ constructor(config, handbookPageRepository, handbookSectionRepository, handbookPageService, modelService, logger) {
39
+ this.config = config;
40
+ this.handbookPageRepository = handbookPageRepository;
41
+ this.handbookSectionRepository = handbookSectionRepository;
42
+ this.handbookPageService = handbookPageService;
43
+ this.modelService = modelService;
44
+ this.logger = logger;
45
+ }
46
+ async sync() {
47
+ // FIRST, before the directory is even read. Chunking is not work that
48
+ // degrades gracefully without AI: every chunk is embedded as it is created
49
+ // and every chunk queues an extraction call, so an unconfigured
50
+ // installation would write a thousand nodes and queue a thousand jobs that
51
+ // all fail inside provider construction. Nothing starts.
52
+ if (!this.modelService.isAiConfigured()) {
53
+ this.logger.warn("Handbook sync refused: no usable AI configuration (AI_PROVIDER / AI_MODEL / EMBEDDER_*).");
54
+ throw new common_1.BadRequestException("AI is not configured: set the AI_* and EMBEDDER_* variables (or an AI connection) before ingesting the handbook.");
55
+ }
56
+ const root = this.config.path;
57
+ if (!root)
58
+ throw new common_1.BadRequestException("No handbook directory configured. Set `handbook.path` on FoundationsModule.forRoot().");
59
+ if (!(0, fs_1.existsSync)(root) || !(0, fs_1.statSync)(root).isDirectory())
60
+ throw new common_1.BadRequestException(`Configured handbook directory does not exist: ${root}`);
61
+ // Read once, before the walk: the index is optional by construction, so an
62
+ // application whose documentation tree has no README still ingests — it
63
+ // just gets prettified section titles and pages with no summary.
64
+ const index = this.readIndex(root);
65
+ // The display translation, when the application ships one. It is a mirror
66
+ // of the English tree under `<root>/<displayPath>` — the same relative
67
+ // paths, translated prose — and it is SHOWN, never INDEXED: the English
68
+ // walk skips it through `exclude`, and the pass at the foot of this method
69
+ // writes its text onto pages the English walk already created. Its own
70
+ // README is read here because the section write below needs it.
71
+ const displayPath = this.config.displayPath ?? "";
72
+ const displayRoot = displayPath.length > 0 ? (0, path_1.join)(root, displayPath) : "";
73
+ const hasDisplay = displayRoot.length > 0 && (0, fs_1.existsSync)(displayRoot) && (0, fs_1.statSync)(displayRoot).isDirectory();
74
+ const displayIndex = hasDisplay ? this.readIndex(displayRoot) : this.emptyIndex();
75
+ const files = this.walk(root, index.summaries);
76
+ const existing = await this.handbookPageRepository.findAllPages();
77
+ const byPath = new Map(existing.map((page) => [page.path, page]));
78
+ const result = { created: 0, updated: 0, unchanged: 0, deleted: 0, failed: [] };
79
+ for (const file of files) {
80
+ const page = byPath.get(file.path);
81
+ try {
82
+ if (!page) {
83
+ const id = (0, crypto_1.randomUUID)();
84
+ await this.handbookPageRepository.createPage({
85
+ id,
86
+ path: file.path,
87
+ title: file.title,
88
+ content: file.content,
89
+ contentHash: file.hash,
90
+ wordCount: file.wordCount,
91
+ section: file.section,
92
+ order: file.order,
93
+ summary: file.summary,
94
+ });
95
+ await this.handbookPageService.chunkAndQueue({
96
+ handbookPageId: id,
97
+ markdown: file.content,
98
+ title: file.title,
99
+ });
100
+ result.created++;
101
+ }
102
+ else if (page.contentHash === file.hash) {
103
+ // The bytes did not change, so the chunks are still correct — but the
104
+ // index fields may be new (a README summary added, a frontmatter
105
+ // `section` corrected), and those cost nothing to rewrite.
106
+ await this.handbookPageRepository.updateMetadata({
107
+ id: page.id,
108
+ title: file.title,
109
+ section: file.section,
110
+ order: file.order,
111
+ summary: file.summary,
112
+ });
113
+ result.unchanged++;
114
+ }
115
+ else {
116
+ await this.handbookPageRepository.updatePage({
117
+ id: page.id,
118
+ title: file.title,
119
+ content: file.content,
120
+ contentHash: file.hash,
121
+ wordCount: file.wordCount,
122
+ section: file.section,
123
+ order: file.order,
124
+ summary: file.summary,
125
+ });
126
+ await this.handbookPageService.chunkAndQueue({
127
+ handbookPageId: page.id,
128
+ markdown: file.content,
129
+ title: file.title,
130
+ });
131
+ result.updated++;
132
+ }
133
+ }
134
+ catch (error) {
135
+ // One unreadable or unchunkable file must not abort the pass: the
136
+ // remaining documentation is still worth indexing, and the caller gets
137
+ // the list of what failed.
138
+ this.logger.error(`Handbook ingest failed for ${file.path}: ${error.message}`);
139
+ result.failed.push(file.path);
140
+ }
141
+ }
142
+ const seen = new Set(files.map((file) => file.path));
143
+ for (const page of existing) {
144
+ if (seen.has(page.path))
145
+ continue;
146
+ // Through the service, not the repository: it is what also drops the
147
+ // page's chunks and resizes the key-concept weights they carried.
148
+ await this.handbookPageService.delete({ id: page.id });
149
+ result.deleted++;
150
+ }
151
+ // Sections come from the files, never from the index. A README's `##`
152
+ // headings are prose sections as often as they are directories — a360's own
153
+ // index carries `Reading orders`, `Conventions`, `Checking the handbook`,
154
+ // `Adding a page`, `Where to look` and `Gaps and drift` — so accepting
155
+ // arbitrary headings would print six empty junk sections on the contents
156
+ // page. A section IS a directory of pages; the index only describes one,
157
+ // contributing a title and a blurb to a key the walk already observed and
158
+ // nothing at all to a key it did not.
159
+ const keys = new Set(files.map((file) => file.section));
160
+ keys.delete("");
161
+ await this.handbookSectionRepository.replaceAll({
162
+ sections: [...keys].sort().map((key) => ({
163
+ id: (0, crypto_1.randomUUID)(),
164
+ key,
165
+ title: index.sections.get(key)?.title ?? this.sectionTitle(key),
166
+ summary: index.sections.get(key)?.summary,
167
+ order: key,
168
+ // The display README describes the same keys — it is a mirror — so a
169
+ // heading it does not carry simply leaves the section untranslated and
170
+ // the reading surface falls back to the English title.
171
+ displayTitle: displayIndex.sections.get(key)?.title,
172
+ displaySummary: displayIndex.sections.get(key)?.summary,
173
+ })),
174
+ });
175
+ if (hasDisplay)
176
+ await this.syncDisplay({ displayRoot, displayIndex });
177
+ this.logger.log(`Handbook sync: ${result.created} created, ${result.updated} updated, ${result.unchanged} unchanged, ${result.deleted} deleted, ${result.failed.length} failed`);
178
+ return result;
179
+ }
180
+ /**
181
+ * Writes the display translation onto the pages the English walk produced.
182
+ *
183
+ * The English tree is the source of truth for WHICH pages exist: a translated
184
+ * file whose path has no English counterpart is counted and skipped, never
185
+ * created. Creating it would put an unindexed page in the manual and — worse,
186
+ * once someone pressed sync again — an English-less page in the retrieval
187
+ * store.
188
+ *
189
+ * Nothing here chunks, embeds or enqueues. `updateDisplay` writes three
190
+ * properties; `content`, `contentHash` and `aiStatus` are untouched, so the
191
+ * index after this pass is byte-for-byte the index before it.
192
+ */
193
+ async syncDisplay(params) {
194
+ const files = this.walk(params.displayRoot, params.displayIndex.summaries);
195
+ // Re-read rather than reuse the map built before the walk: on a first sync
196
+ // that map is empty, because every page was created moments ago, and every
197
+ // translation would be reported as an orphan.
198
+ const pages = await this.handbookPageRepository.findAllPages();
199
+ const byPath = new Map(pages.map((page) => [page.path, page]));
200
+ let translated = 0;
201
+ let orphaned = 0;
202
+ for (const file of files) {
203
+ const page = byPath.get(file.path);
204
+ if (!page) {
205
+ orphaned++;
206
+ continue;
207
+ }
208
+ await this.handbookPageRepository.updateDisplay({
209
+ id: page.id,
210
+ displayTitle: file.title,
211
+ displaySummary: file.summary,
212
+ displayContent: file.content,
213
+ });
214
+ translated++;
215
+ }
216
+ this.logger.log(`Handbook display translation (${this.config.displayPath}): ${translated} pages translated, ${orphaned} translated files with no English counterpart`);
217
+ }
218
+ walk(root, summaries = new Map()) {
219
+ const out = [];
220
+ const visit = (directory) => {
221
+ for (const entry of (0, fs_1.readdirSync)(directory, { withFileTypes: true })) {
222
+ const absolute = (0, path_1.join)(directory, entry.name);
223
+ if (entry.isDirectory()) {
224
+ visit(absolute);
225
+ continue;
226
+ }
227
+ if (!entry.name.endsWith(".md"))
228
+ continue;
229
+ const path = (0, path_1.relative)(root, absolute).split(path_1.sep).join("/");
230
+ if (this.isExcluded(path))
231
+ continue;
232
+ const raw = (0, fs_1.readFileSync)(absolute, "utf8");
233
+ const { frontmatter, body } = this.splitFrontmatter(raw);
234
+ // Declared section first, then the directory the file sits in. A file at
235
+ // the tree root with no declaration gets "", and is dropped from the
236
+ // section list rather than inventing an unnamed one.
237
+ const section = frontmatter.section ?? path.split("/").slice(0, -1)[0] ?? "";
238
+ out.push({
239
+ path,
240
+ section,
241
+ order: path.toLowerCase(),
242
+ summary: summaries.get(path),
243
+ // The BODY is what gets stored, rendered and chunked. Frontmatter is
244
+ // metadata about the file, not prose about the subject: embedding
245
+ // `sources_verified: 2026-09-10` teaches the retriever nothing and
246
+ // dilutes the chunk it lands in.
247
+ content: body,
248
+ title: this.titleOf({ frontmatterTitle: frontmatter.title, content: body, path }),
249
+ // Hashed over the RAW file (so editing only the frontmatter still
250
+ // re-ingests the page) plus the format version (so changing the parser
251
+ // re-ingests every page).
252
+ hash: (0, crypto_1.createHash)("sha256").update(`${INGEST_FORMAT_VERSION}\n${raw}`).digest("hex"),
253
+ wordCount: body.split(/\s+/).filter(Boolean).length,
254
+ });
255
+ }
256
+ };
257
+ visit(root);
258
+ return out;
259
+ }
260
+ /**
261
+ * Glob support is deliberately the smallest thing that serves the config:
262
+ * a literal prefix with a trailing `**`, or an exact path. No dependency,
263
+ * nothing to misread.
264
+ */
265
+ isExcluded(path) {
266
+ return this.config.exclude.some((pattern) => {
267
+ if (pattern.endsWith("/**"))
268
+ return path.startsWith(pattern.slice(0, -2));
269
+ if (pattern.endsWith("**"))
270
+ return path.startsWith(pattern.slice(0, -2));
271
+ return path === pattern;
272
+ });
273
+ }
274
+ /**
275
+ * Splits a leading YAML frontmatter block off a markdown file and returns its
276
+ * scalar keys.
277
+ *
278
+ * Still not a YAML parser: the block this reads is a handful of `key: value`
279
+ * lines, and a dependency buys nothing but new failure modes on a malformed
280
+ * block. Nested structures and lists are ignored rather than half-parsed.
281
+ */
282
+ splitFrontmatter(raw) {
283
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
284
+ if (!match)
285
+ return { frontmatter: {}, body: raw };
286
+ const frontmatter = {};
287
+ for (const line of match[1].split(/\r?\n/)) {
288
+ const pair = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
289
+ if (!pair)
290
+ continue;
291
+ const value = pair[2].trim().replace(/^["']|["']$/g, "");
292
+ if (value)
293
+ frontmatter[pair[1]] = value;
294
+ }
295
+ return { frontmatter, body: raw.slice(match[0].length).trimStart() };
296
+ }
297
+ /**
298
+ * The tree's own `README.md` index, parsed — or empty maps when it has none.
299
+ *
300
+ * Both roots go through this: the English tree and, when one is configured,
301
+ * the display tree. A display README's link paths are relative to the display
302
+ * root, which IS the English relative path, so the summaries it yields key
303
+ * straight onto the English pages.
304
+ */
305
+ readIndex(directory) {
306
+ const indexPath = (0, path_1.join)(directory, "README.md");
307
+ return (0, fs_1.existsSync)(indexPath) ? this.parseIndex((0, fs_1.readFileSync)(indexPath, "utf8")) : this.emptyIndex();
308
+ }
309
+ emptyIndex() {
310
+ return { sections: new Map(), summaries: new Map() };
311
+ }
312
+ /**
313
+ * Reads the tree's own `README.md` index, when it has one.
314
+ *
315
+ * Two maps come out: section key to its heading title and blurb, and
316
+ * repo-relative path to the one-line description the index gives it. Both are
317
+ * optional by construction — an application with no README gets empty maps,
318
+ * prettified section titles and pages with no summary, and the manual still
319
+ * renders.
320
+ */
321
+ parseIndex(readme) {
322
+ const sections = new Map();
323
+ const summaries = new Map();
324
+ const blocks = readme.split(/^##\s+/m).slice(1);
325
+ for (const block of blocks) {
326
+ const lines = block.split(/\r?\n/);
327
+ const { key, title } = this.headingOf(lines[0]);
328
+ sections.set(key, { title, summary: this.blurbOf(lines.slice(1)) });
329
+ }
330
+ // `- [Title](path) — summary` across the whole file, section blocks included.
331
+ const entry = /^-\s*\[[^\]]+\]\(([^)]+)\)\s*[—–-]\s*(.+)$/gm;
332
+ for (const match of readme.matchAll(entry)) {
333
+ const path = match[1].split("#")[0].trim();
334
+ if (path.length > 0)
335
+ summaries.set(path, match[2].trim());
336
+ }
337
+ return { sections, summaries };
338
+ }
339
+ /**
340
+ * Splits a section heading into the key it names and the title to print.
341
+ *
342
+ * Two forms are accepted:
343
+ *
344
+ * - `## 03-backend` — the bare directory key. The title is derived from it,
345
+ * which is all a tree with no authored section names can offer.
346
+ * - `## 03-backend — Backend` — the key, a dash, and an authored title. The
347
+ * title is taken as written.
348
+ *
349
+ * The second form is the only place a section name can be written at all: a
350
+ * section is a DIRECTORY, and a directory has no front matter to carry a
351
+ * title. It is what lets a translated index name its sections in its own
352
+ * language, and what lets an English one write "AI" where derivation would
353
+ * sentence-case `06-ai` into "Ai".
354
+ *
355
+ * The separator must carry whitespace on both sides. Keys contain hyphens
356
+ * (`00-start-here`), so a bare `-` would split the key itself.
357
+ */
358
+ headingOf(heading) {
359
+ const trimmed = heading.trim();
360
+ const split = trimmed.match(/^(\S+)\s+[—–-]\s+(.+)$/);
361
+ if (!split)
362
+ return { key: trimmed, title: this.sectionTitle(trimmed) };
363
+ return { key: split[1], title: split[2].trim() };
364
+ }
365
+ /**
366
+ * The paragraph beneath a section heading, rebuilt into one sentence.
367
+ *
368
+ * A README wraps its prose, so the blurb arrives as several lines; joining
369
+ * the contiguous run is the difference between a summary and a sentence cut
370
+ * at "and an index". Collection starts at the first line that is neither
371
+ * blank, nor a list item, nor inside a fenced code block, and stops at the
372
+ * first blank line or list item after that.
373
+ *
374
+ * Fenced blocks are stepped over whole: a section that opens with one — the
375
+ * a360 handbook's own `## Checking the handbook` does — must be summarised by
376
+ * its prose, not by a fence or by the command inside it.
377
+ */
378
+ blurbOf(lines) {
379
+ const paragraph = [];
380
+ let fenced = false;
381
+ for (const raw of lines) {
382
+ const line = raw.trim();
383
+ if (line.startsWith("```")) {
384
+ fenced = !fenced;
385
+ continue;
386
+ }
387
+ if (fenced)
388
+ continue;
389
+ if (line.length === 0 || line.startsWith("-")) {
390
+ if (paragraph.length > 0)
391
+ break;
392
+ continue;
393
+ }
394
+ paragraph.push(line);
395
+ }
396
+ return paragraph.length > 0 ? paragraph.join(" ") : undefined;
397
+ }
398
+ /**
399
+ * `00-start-here` becomes "Start here". The numeric prefix orders the
400
+ * directories on disk and is noise on screen; a key with no prefix is used
401
+ * as written.
402
+ */
403
+ sectionTitle(key) {
404
+ const words = key.replace(/^\d+-/, "").replace(/-/g, " ").trim();
405
+ if (words.length === 0)
406
+ return key;
407
+ return words.charAt(0).toUpperCase() + words.slice(1);
408
+ }
409
+ /**
410
+ * Frontmatter `title` first, then the first H1, then the filename. The
411
+ * handbook this was built against uses frontmatter and no H1 at all, so the
412
+ * H1 branch exists for directories that are written the other way.
413
+ */
414
+ titleOf(params) {
415
+ if (params.frontmatterTitle)
416
+ return params.frontmatterTitle;
417
+ const heading = params.content.match(/^#\s+(.+)$/m);
418
+ if (heading)
419
+ return heading[1].trim();
420
+ const name = params.path.split("/").pop() ?? params.path;
421
+ return name.replace(/\.md$/, "");
422
+ }
423
+ };
424
+ exports.HandbookIngestService = HandbookIngestService;
425
+ exports.HandbookIngestService = HandbookIngestService = __decorate([
426
+ (0, common_1.Injectable)(),
427
+ __param(0, (0, common_1.Inject)(handbook_config_interface_1.HANDBOOK_CONFIG)),
428
+ __metadata("design:paramtypes", [Object, handbook_page_repository_1.HandbookPageRepository,
429
+ handbook_section_repository_1.HandbookSectionRepository,
430
+ handbook_page_service_1.HandbookPageService,
431
+ model_service_1.ModelService,
432
+ logging_service_1.AppLoggingService])
433
+ ], HandbookIngestService);
434
+ //# sourceMappingURL=handbook-ingest.service.js.map