@carlonicora/nestjs-neo4jsonapi 2.14.11 → 2.15.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 (121) hide show
  1. package/dist/agents/contextualiser/contexts/contextualiser.context.d.ts +41 -0
  2. package/dist/agents/contextualiser/contexts/contextualiser.context.d.ts.map +1 -1
  3. package/dist/agents/contextualiser/contexts/contextualiser.context.js +17 -0
  4. package/dist/agents/contextualiser/contexts/contextualiser.context.js.map +1 -1
  5. package/dist/agents/contextualiser/contexts/notebook.context.d.ts +22 -0
  6. package/dist/agents/contextualiser/contexts/notebook.context.d.ts.map +1 -1
  7. package/dist/agents/contextualiser/contexts/notebook.context.js +14 -0
  8. package/dist/agents/contextualiser/contexts/notebook.context.js.map +1 -1
  9. package/dist/agents/contextualiser/factories/contextualiser.context.factory.d.ts.map +1 -1
  10. package/dist/agents/contextualiser/factories/contextualiser.context.factory.js +2 -0
  11. package/dist/agents/contextualiser/factories/contextualiser.context.factory.js.map +1 -1
  12. package/dist/agents/contextualiser/nodes/atomicfacts.node.service.d.ts.map +1 -1
  13. package/dist/agents/contextualiser/nodes/atomicfacts.node.service.js +17 -5
  14. package/dist/agents/contextualiser/nodes/atomicfacts.node.service.js.map +1 -1
  15. package/dist/agents/contextualiser/nodes/chunk.node.service.d.ts +1 -14
  16. package/dist/agents/contextualiser/nodes/chunk.node.service.d.ts.map +1 -1
  17. package/dist/agents/contextualiser/nodes/chunk.node.service.js +56 -206
  18. package/dist/agents/contextualiser/nodes/chunk.node.service.js.map +1 -1
  19. package/dist/agents/contextualiser/nodes/chunk.vector.node.service.d.ts +3 -12
  20. package/dist/agents/contextualiser/nodes/chunk.vector.node.service.d.ts.map +1 -1
  21. package/dist/agents/contextualiser/nodes/chunk.vector.node.service.js +44 -133
  22. package/dist/agents/contextualiser/nodes/chunk.vector.node.service.js.map +1 -1
  23. package/dist/agents/contextualiser/nodes/keyconcepts.node.service.d.ts +1 -2
  24. package/dist/agents/contextualiser/nodes/keyconcepts.node.service.d.ts.map +1 -1
  25. package/dist/agents/contextualiser/nodes/keyconcepts.node.service.js +60 -60
  26. package/dist/agents/contextualiser/nodes/keyconcepts.node.service.js.map +1 -1
  27. package/dist/agents/contextualiser/nodes/question.refiner.node.service.d.ts.map +1 -1
  28. package/dist/agents/contextualiser/nodes/question.refiner.node.service.js +7 -2
  29. package/dist/agents/contextualiser/nodes/question.refiner.node.service.js.map +1 -1
  30. package/dist/agents/contextualiser/nodes/rational.node.service.js +2 -1
  31. package/dist/agents/contextualiser/nodes/rational.node.service.js.map +1 -1
  32. package/dist/agents/contextualiser/services/contextualiser.service.d.ts.map +1 -1
  33. package/dist/agents/contextualiser/services/contextualiser.service.js +30 -22
  34. package/dist/agents/contextualiser/services/contextualiser.service.js.map +1 -1
  35. package/dist/agents/prompts/index.d.ts +0 -2
  36. package/dist/agents/prompts/index.d.ts.map +1 -1
  37. package/dist/agents/prompts/index.js +7 -5
  38. package/dist/agents/prompts/index.js.map +1 -1
  39. package/dist/agents/responder/contexts/responder.context.d.ts +96 -0
  40. package/dist/agents/responder/contexts/responder.context.d.ts.map +1 -1
  41. package/dist/agents/responder/interfaces/unified.trace.interface.d.ts +9 -0
  42. package/dist/agents/responder/interfaces/unified.trace.interface.d.ts.map +1 -1
  43. package/dist/agents/responder/nodes/__tests__/helpers/answer-node.harness.d.ts +38 -0
  44. package/dist/agents/responder/nodes/__tests__/helpers/answer-node.harness.d.ts.map +1 -0
  45. package/dist/agents/responder/nodes/__tests__/helpers/answer-node.harness.js +36 -0
  46. package/dist/agents/responder/nodes/__tests__/helpers/answer-node.harness.js.map +1 -0
  47. package/dist/agents/responder/nodes/graph.node.service.d.ts.map +1 -1
  48. package/dist/agents/responder/nodes/graph.node.service.js +18 -3
  49. package/dist/agents/responder/nodes/graph.node.service.js.map +1 -1
  50. package/dist/agents/responder/nodes/planner.node.service.d.ts.map +1 -1
  51. package/dist/agents/responder/nodes/planner.node.service.js +9 -0
  52. package/dist/agents/responder/nodes/planner.node.service.js.map +1 -1
  53. package/dist/agents/responder/nodes/responder.answer.node.service.d.ts +26 -1
  54. package/dist/agents/responder/nodes/responder.answer.node.service.d.ts.map +1 -1
  55. package/dist/agents/responder/nodes/responder.answer.node.service.js +127 -13
  56. package/dist/agents/responder/nodes/responder.answer.node.service.js.map +1 -1
  57. package/dist/agents/responder/services/responder.service.d.ts.map +1 -1
  58. package/dist/agents/responder/services/responder.service.js +4 -0
  59. package/dist/agents/responder/services/responder.service.js.map +1 -1
  60. package/dist/common/utils/settled.d.ts +33 -0
  61. package/dist/common/utils/settled.d.ts.map +1 -0
  62. package/dist/common/utils/settled.js +66 -0
  63. package/dist/common/utils/settled.js.map +1 -0
  64. package/dist/config/interfaces/config.prompts.interface.d.ts +12 -0
  65. package/dist/config/interfaces/config.prompts.interface.d.ts.map +1 -1
  66. package/dist/eval/retrieval/end-to-end-sweep.service.d.ts +52 -0
  67. package/dist/eval/retrieval/end-to-end-sweep.service.d.ts.map +1 -0
  68. package/dist/eval/retrieval/end-to-end-sweep.service.js +194 -0
  69. package/dist/eval/retrieval/end-to-end-sweep.service.js.map +1 -0
  70. package/dist/eval/retrieval/evidence.matcher.d.ts +23 -0
  71. package/dist/eval/retrieval/evidence.matcher.d.ts.map +1 -0
  72. package/dist/eval/retrieval/evidence.matcher.js +38 -0
  73. package/dist/eval/retrieval/evidence.matcher.js.map +1 -0
  74. package/dist/eval/retrieval/index.d.ts +10 -0
  75. package/dist/eval/retrieval/index.d.ts.map +1 -0
  76. package/dist/eval/retrieval/index.js +35 -0
  77. package/dist/eval/retrieval/index.js.map +1 -0
  78. package/dist/eval/retrieval/question-set.loader.d.ts +16 -0
  79. package/dist/eval/retrieval/question-set.loader.d.ts.map +1 -0
  80. package/dist/eval/retrieval/question-set.loader.js +73 -0
  81. package/dist/eval/retrieval/question-set.loader.js.map +1 -0
  82. package/dist/eval/retrieval/retrieval-eval.module.d.ts +14 -0
  83. package/dist/eval/retrieval/retrieval-eval.module.d.ts.map +1 -0
  84. package/dist/eval/retrieval/retrieval-eval.module.js +48 -0
  85. package/dist/eval/retrieval/retrieval-eval.module.js.map +1 -0
  86. package/dist/eval/retrieval/retrieval-eval.types.d.ts +84 -0
  87. package/dist/eval/retrieval/retrieval-eval.types.d.ts.map +1 -0
  88. package/dist/eval/retrieval/retrieval-eval.types.js +25 -0
  89. package/dist/eval/retrieval/retrieval-eval.types.js.map +1 -0
  90. package/dist/eval/retrieval/retrieval-sweep.service.d.ts +35 -0
  91. package/dist/eval/retrieval/retrieval-sweep.service.d.ts.map +1 -0
  92. package/dist/eval/retrieval/retrieval-sweep.service.js +93 -0
  93. package/dist/eval/retrieval/retrieval-sweep.service.js.map +1 -0
  94. package/dist/eval/retrieval/rubric-judge.service.d.ts +30 -0
  95. package/dist/eval/retrieval/rubric-judge.service.d.ts.map +1 -0
  96. package/dist/eval/retrieval/rubric-judge.service.js +138 -0
  97. package/dist/eval/retrieval/rubric-judge.service.js.map +1 -0
  98. package/dist/eval/retrieval/sweep-report.d.ts +11 -0
  99. package/dist/eval/retrieval/sweep-report.d.ts.map +1 -0
  100. package/dist/eval/retrieval/sweep-report.js +79 -0
  101. package/dist/eval/retrieval/sweep-report.js.map +1 -0
  102. package/dist/eval/retrieval/token-accounting.check.d.ts +39 -0
  103. package/dist/eval/retrieval/token-accounting.check.d.ts.map +1 -0
  104. package/dist/eval/retrieval/token-accounting.check.js +75 -0
  105. package/dist/eval/retrieval/token-accounting.check.js.map +1 -0
  106. package/dist/foundations/atomicfact/repositories/atomicfact.repository.d.ts.map +1 -1
  107. package/dist/foundations/atomicfact/repositories/atomicfact.repository.js +7 -0
  108. package/dist/foundations/atomicfact/repositories/atomicfact.repository.js.map +1 -1
  109. package/dist/foundations/chunk/repositories/chunk.repository.d.ts +87 -1
  110. package/dist/foundations/chunk/repositories/chunk.repository.d.ts.map +1 -1
  111. package/dist/foundations/chunk/repositories/chunk.repository.js +239 -50
  112. package/dist/foundations/chunk/repositories/chunk.repository.js.map +1 -1
  113. package/dist/foundations/chunk/repositories/retrieval.constants.d.ts +69 -0
  114. package/dist/foundations/chunk/repositories/retrieval.constants.d.ts.map +1 -0
  115. package/dist/foundations/chunk/repositories/retrieval.constants.js +72 -0
  116. package/dist/foundations/chunk/repositories/retrieval.constants.js.map +1 -0
  117. package/dist/foundations/keyconcept/repositories/keyconcept.repository.d.ts +2 -0
  118. package/dist/foundations/keyconcept/repositories/keyconcept.repository.d.ts.map +1 -1
  119. package/dist/foundations/keyconcept/repositories/keyconcept.repository.js +9 -5
  120. package/dist/foundations/keyconcept/repositories/keyconcept.repository.js.map +1 -1
  121. package/package.json +1 -1
@@ -0,0 +1,75 @@
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 TokenAccountingChecker_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.TokenAccountingChecker = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ /** Tokens may legitimately differ by rounding; anything larger is a defect. */
13
+ const TOLERANCE_TOKENS = 2;
14
+ /**
15
+ * Cross-checks a turn's reported token aggregate against the sum of the calls
16
+ * actually observed for it.
17
+ *
18
+ * Spec §6.5. The `2C + A` reducer bug inflated every archived chat figure and
19
+ * went unnoticed for a full measurement cycle; this check is what makes the
20
+ * same class of bug loud instead of silent. Provider-reported per-call figures
21
+ * are the source of truth.
22
+ */
23
+ let TokenAccountingChecker = TokenAccountingChecker_1 = class TokenAccountingChecker {
24
+ constructor() {
25
+ this.logger = new common_1.Logger(TokenAccountingChecker_1.name);
26
+ this.observedInput = 0;
27
+ this.observedOutput = 0;
28
+ }
29
+ /** Record one provider call's reported usage for the question in flight. */
30
+ observe(params) {
31
+ this.observedInput += params.input;
32
+ this.observedOutput += params.output;
33
+ }
34
+ /**
35
+ * Clears the accumulator WITHOUT producing a verdict.
36
+ *
37
+ * For the question that never completed a turn: it has nothing to compare, but
38
+ * it may still have deposited observations before it threw. Leaving them in
39
+ * place attributes them to the NEXT question — which either invents a mismatch
40
+ * that is not there or, worse, masks one that is. `check()` resets as a side
41
+ * effect of comparing; this is the same reset for the path that cannot compare.
42
+ */
43
+ reset() {
44
+ this.observedInput = 0;
45
+ this.observedOutput = 0;
46
+ }
47
+ /** Compare and RESET, so the next question starts clean. */
48
+ check(params) {
49
+ const result = {
50
+ ledgerInput: params.ledger.input,
51
+ observedInput: this.observedInput,
52
+ ledgerOutput: params.ledger.output,
53
+ observedOutput: this.observedOutput,
54
+ agrees: Math.abs(params.ledger.input - this.observedInput) <= TOLERANCE_TOKENS &&
55
+ Math.abs(params.ledger.output - this.observedOutput) <= TOLERANCE_TOKENS,
56
+ // The comparison ran. Only this method may set it — every other producer
57
+ // of a `TokenAccountingCheck` is describing a check that did NOT happen.
58
+ checked: true,
59
+ };
60
+ if (!result.agrees) {
61
+ this.logger.error(`token accounting MISMATCH on ${params.questionId}: reported ` +
62
+ `${result.ledgerInput}/${result.ledgerOutput} but observed ` +
63
+ `${result.observedInput}/${result.observedOutput}. The baseline is not trustworthy ` +
64
+ `until this is explained.`);
65
+ }
66
+ this.observedInput = 0;
67
+ this.observedOutput = 0;
68
+ return result;
69
+ }
70
+ };
71
+ exports.TokenAccountingChecker = TokenAccountingChecker;
72
+ exports.TokenAccountingChecker = TokenAccountingChecker = TokenAccountingChecker_1 = __decorate([
73
+ (0, common_1.Injectable)()
74
+ ], TokenAccountingChecker);
75
+ //# sourceMappingURL=token-accounting.check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-accounting.check.js","sourceRoot":"","sources":["../../../src/eval/retrieval/token-accounting.check.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,2CAAoD;AAGpD,+EAA+E;AAC/E,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAE3B;;;;;;;;GAQG;AAEI,IAAM,sBAAsB,8BAA5B,MAAM,sBAAsB;IAA5B;QACY,WAAM,GAAG,IAAI,eAAM,CAAC,wBAAsB,CAAC,IAAI,CAAC,CAAC;QAC1D,kBAAa,GAAG,CAAC,CAAC;QAClB,mBAAc,GAAG,CAAC,CAAC;IAkD7B,CAAC;IAhDC,4EAA4E;IAC5E,OAAO,CAAC,MAAyC;QAC/C,IAAI,CAAC,aAAa,IAAI,MAAM,CAAC,KAAK,CAAC;QACnC,IAAI,CAAC,cAAc,IAAI,MAAM,CAAC,MAAM,CAAC;IACvC,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK;QACH,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;QACvB,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED,4DAA4D;IAC5D,KAAK,CAAC,MAAyE;QAC7E,MAAM,MAAM,GAAyB;YACnC,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK;YAChC,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM;YAClC,cAAc,EAAE,IAAI,CAAC,cAAc;YACnC,MAAM,EACJ,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,gBAAgB;gBACtE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,gBAAgB;YAC1E,yEAAyE;YACzE,yEAAyE;YACzE,OAAO,EAAE,IAAI;SACd,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACnB,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,gCAAgC,MAAM,CAAC,UAAU,aAAa;gBAC5D,GAAG,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,YAAY,gBAAgB;gBAC5D,GAAG,MAAM,CAAC,aAAa,IAAI,MAAM,CAAC,cAAc,oCAAoC;gBACpF,0BAA0B,CAC7B,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;QACvB,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC;QACxB,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAA;AArDY,wDAAsB;iCAAtB,sBAAsB;IADlC,IAAA,mBAAU,GAAE;GACA,sBAAsB,CAqDlC"}
@@ -1 +1 @@
1
- {"version":3,"file":"atomicfact.repository.d.ts","sourceRoot":"","sources":["../../../../src/foundations/atomicfact/repositories/atomicfact.repository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iDAAiD,CAAC;AAC1F,OAAO,EAAmB,qBAAqB,EAAE,MAAM,uDAAuD,CAAC;AAC/G,OAAO,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,4CAA4C,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,kDAAkD,CAAC;AACnF,OAAO,EAAE,UAAU,EAAE,MAAM,8CAA8C,CAAC;AAG1E,qBACa,oBAAqB,YAAW,YAAY;IAErD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU;IACF,OAAO,CAAC,QAAQ,CAAC,aAAa;IACvD,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAJV,KAAK,EAAE,YAAY,EACnB,eAAe,EAAE,eAAe,EAChC,UAAU,EAAE,UAAU,EACG,aAAa,EAAE,qBAAqB,EAC7D,UAAU,EAAE,uBAAuB;IAGhD,YAAY;IAMZ,4BAA4B,CAAC,MAAM,EAAE;QACzC,WAAW,EAAE,MAAM,EAAE,CAAC;QACtB,YAAY,EAAE,MAAM,EAAE,CAAC;QACvB,iBAAiB,EAAE,MAAM,EAAE,CAAC;QAC5B,UAAU,EAAE,UAAU,CAAC;KACxB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAkCnB,wBAAwB,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAgB5E,kBAAkB,CAAC,MAAM,EAAE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC;IAezE,6BAA6B,IAAI,OAAO,CAAC,IAAI,CAAC;IAY9C,gBAAgB,CAAC,MAAM,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAmB1G"}
1
+ {"version":3,"file":"atomicfact.repository.d.ts","sourceRoot":"","sources":["../../../../src/foundations/atomicfact/repositories/atomicfact.repository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iDAAiD,CAAC;AAC1F,OAAO,EAAmB,qBAAqB,EAAE,MAAM,uDAAuD,CAAC;AAC/G,OAAO,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,4CAA4C,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,kDAAkD,CAAC;AACnF,OAAO,EAAE,UAAU,EAAE,MAAM,8CAA8C,CAAC;AAI1E,qBACa,oBAAqB,YAAW,YAAY;IAErD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU;IACF,OAAO,CAAC,QAAQ,CAAC,aAAa;IACvD,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAJV,KAAK,EAAE,YAAY,EACnB,eAAe,EAAE,eAAe,EAChC,UAAU,EAAE,UAAU,EACG,aAAa,EAAE,qBAAqB,EAC7D,UAAU,EAAE,uBAAuB;IAGhD,YAAY;IAMZ,4BAA4B,CAAC,MAAM,EAAE;QACzC,WAAW,EAAE,MAAM,EAAE,CAAC;QACtB,YAAY,EAAE,MAAM,EAAE,CAAC;QACvB,iBAAiB,EAAE,MAAM,EAAE,CAAC;QAC5B,UAAU,EAAE,UAAU,CAAC;KACxB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAwCnB,wBAAwB,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAgB5E,kBAAkB,CAAC,MAAM,EAAE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC;IAezE,6BAA6B,IAAI,OAAO,CAAC,IAAI,CAAC;IAY9C,gBAAgB,CAAC,MAAM,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAmB1G"}
@@ -20,6 +20,7 @@ const ai_source_query_provider_1 = require("../../../common/repositories/ai-sour
20
20
  const neo4j_service_1 = require("../../../core/neo4j/services/neo4j.service");
21
21
  const security_service_1 = require("../../../core/security/services/security.service");
22
22
  const atomic_fact_model_1 = require("../../atomicfact/entities/atomic.fact.model");
23
+ const retrieval_constants_1 = require("../../chunk/repositories/retrieval.constants");
23
24
  let AtomicFactRepository = class AtomicFactRepository {
24
25
  constructor(neo4j, securityService, clsService, aiSourceQuery, agentScope) {
25
26
  this.neo4j = neo4j;
@@ -51,9 +52,14 @@ let AtomicFactRepository = class AtomicFactRepository {
51
52
  keyConcepts: params.keyConcepts,
52
53
  skipChunkIds: params.skipChunkIds,
53
54
  skipAtomicFactIds: params.skipAtomicFactIds,
55
+ maxAtomicFacts: retrieval_constants_1.MAX_ATOMIC_FACTS,
54
56
  ...scope.params,
55
57
  ...scopeFilter.params,
56
58
  };
59
+ // Deliberate: the facts are NOT ordered before the cut. Atomic facts carry no
60
+ // embedding (spec §2.4), so there is no relevance signal to order by — this is
61
+ // a spend bound, not a ranking. Giving facts embeddings is out of scope and has
62
+ // its own spec.
57
63
  query.query += `
58
64
  ${scope.cypher}
59
65
  ${scopeFilter.cypher}
@@ -62,6 +68,7 @@ let AtomicFactRepository = class AtomicFactRepository {
62
68
  ${params.skipChunkIds.length > 0 ? `AND NOT atomicfact_chunk.id IN $skipChunkIds` : ""}
63
69
  ${params.skipAtomicFactIds.length > 0 ? `AND NOT atomicfact.id IN $skipAtomicFactIds` : ""}
64
70
  RETURN atomicfact, atomicfact_chunk
71
+ LIMIT toInteger($maxAtomicFacts)
65
72
  `;
66
73
  return this.neo4j.readMany(query);
67
74
  }
@@ -1 +1 @@
1
- {"version":3,"file":"atomicfact.repository.js","sourceRoot":"","sources":["../../../../src/foundations/atomicfact/repositories/atomicfact.repository.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"atomicfact.repository.js","sourceRoot":"","sources":["../../../../src/foundations/atomicfact/repositories/atomicfact.repository.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAkE;AAClE,2CAAwC;AACxC,wFAA0F;AAC1F,oGAA+G;AAE/G,8EAA0E;AAC1E,uFAAmF;AAEnF,mFAA8E;AAC9E,sFAAgF;AAGzE,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IAC/B,YACmB,KAAmB,EACnB,eAAgC,EAChC,UAAsB,EACG,aAAoC,EAC7D,UAAmC;QAJnC,UAAK,GAAL,KAAK,CAAc;QACnB,oBAAe,GAAf,eAAe,CAAiB;QAChC,eAAU,GAAV,UAAU,CAAY;QACG,kBAAa,GAAb,aAAa,CAAuB;QAC7D,eAAU,GAAV,UAAU,CAAyB;IACnD,CAAC;IAEJ,KAAK,CAAC,YAAY;QAChB,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;YACxB,KAAK,EAAE,2GAA2G;SACnH,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,4BAA4B,CAAC,MAKlC;QACC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,mCAAe,EAAE,CAAC,CAAC;QACpE,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC;YACrC,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,aAAa,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC5C,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;QACH,0EAA0E;QAC1E,4EAA4E;QAC5E,wEAAwE;QACxE,0BAA0B;QAC1B,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;QAE5F,KAAK,CAAC,WAAW,GAAG;YAClB,GAAG,KAAK,CAAC,WAAW;YACpB,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,iBAAiB,EAAE,MAAM,CAAC,iBAAiB;YAC3C,cAAc,EAAE,sCAAgB;YAChC,GAAG,KAAK,CAAC,MAAM;YACf,GAAG,WAAW,CAAC,MAAM;SACtB,CAAC;QACF,8EAA8E;QAC9E,+EAA+E;QAC/E,gFAAgF;QAChF,gBAAgB;QAChB,KAAK,CAAC,KAAK,IAAI;QACX,KAAK,CAAC,MAAM;QACZ,WAAW,CAAC,MAAM;;;QAGlB,MAAM,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,8CAA8C,CAAC,CAAC,CAAC,EAAE;QACpF,MAAM,CAAC,iBAAiB,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,6CAA6C,CAAC,CAAC,CAAC,EAAE;;;GAG7F,CAAC;QACA,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,wBAAwB,CAAC,MAA2B;QACxD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,mCAAe,EAAE,CAAC,CAAC;QAEpE,KAAK,CAAC,WAAW,GAAG;YAClB,GAAG,KAAK,CAAC,WAAW;YACpB,OAAO,EAAE,MAAM,CAAC,OAAO;SACxB,CAAC;QAEF,KAAK,CAAC,KAAK,IAAI;;;KAGd,CAAC;QAEF,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,MAAgC;QACvD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,mCAAe,EAAE,CAAC,CAAC;QAEpE,KAAK,CAAC,WAAW,GAAG;YAClB,GAAG,KAAK,CAAC,WAAW;YACpB,YAAY,EAAE,MAAM,CAAC,YAAY;SAClC,CAAC;QAEF,KAAK,CAAC,KAAK,IAAI;;;KAGd,CAAC;QACF,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,6BAA6B;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;QAErC,KAAK,CAAC,KAAK,GAAG;;;;KAIb,CAAC;QAEF,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,MAAkE;QACvF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;QAErC,KAAK,CAAC,WAAW,GAAG;YAClB,GAAG,KAAK,CAAC,WAAW;YACpB,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,iBAAiB,EAAE,MAAM,CAAC,OAAO;SAClC,CAAC;QAEF,KAAK,CAAC,KAAK,IAAI;;;;;KAKd,CAAC;QAEF,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC;CACF,CAAA;AA1HY,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,mBAAU,GAAE;IAMR,WAAA,IAAA,eAAM,EAAC,0CAAe,CAAC,CAAA;qCAHA,4BAAY;QACF,kCAAe;QACpB,uBAAU,UAEV,4CAAuB;GAN3C,oBAAoB,CA0HhC"}
@@ -39,13 +39,70 @@ export declare class ChunkRepository implements OnModuleInit {
39
39
  * lives with the caller (the contextualiser knows which content the run is
40
40
  * bound to), which is why it is passed down rather than derived here. Absent,
41
41
  * `EmbedderService.persistUsage` records nothing.
42
+ *
43
+ * Each returned chunk carries `score`: the cosine similarity of that chunk against
44
+ * the question embedding. Both retrieval halves have to reach the notebook on ONE
45
+ * scale — the answer node orders entries best-score-first and fills a character
46
+ * budget, so an unscored half sorts last however good it is. The RRF score is NOT
47
+ * usable for that: it is rank-derived (the top hit scores ≈1/61 whether it is a
48
+ * perfect match or noise) and is not comparable with a cosine from the graph half.
42
49
  */
43
50
  findPotentialChunks(params: {
44
51
  question: string;
45
52
  dataLimits: DataLimits;
46
53
  attribution?: EmbedderAttribution;
47
- }): Promise<Chunk[]>;
54
+ /**
55
+ * Precomputed question embedding. When supplied the repository does NOT
56
+ * embed again — the same question is otherwise embedded twice per turn,
57
+ * once here and once in findPotentialKeyConcepts. Optional so existing
58
+ * callers are unaffected.
59
+ */
60
+ queryEmbedding?: number[];
61
+ }): Promise<Array<Chunk & {
62
+ score?: number;
63
+ }>>;
64
+ /**
65
+ * Exact cosine over the scoped set. No recall cliff by construction: every
66
+ * in-scope chunk is scored, so a small tenant can never be crowded out of a
67
+ * global top-K by a large one.
68
+ */
69
+ private vectorIdsByExactScan;
70
+ /**
71
+ * Index-backed fallback for scoped sets too large to scan exactly. Still
72
+ * post-filters, so it still has a cliff — the over-fetch is what pushes that
73
+ * cliff out of reach, and it is deliberately far above the previous 1,000.
74
+ */
75
+ private vectorIdsByIndex;
76
+ /**
77
+ * Lexical branch, scoped the same way. Previously filtered against the
78
+ * client-side id list; now joined in the database like the vector branch.
79
+ */
80
+ private lexicalIdsInScope;
81
+ /**
82
+ * INPUT ORDER IS PART OF THE CONTRACT here too: `WHERE chunk.id IN $ids` yields rows
83
+ * in store order, and the fused RRF order is what the caller means by "best first".
84
+ *
85
+ * `queryEmbedding` attaches the cosine score of each chunk against the question, on
86
+ * the same scale `findChunksByIds` puts on the graph half. No floor and no count cap
87
+ * are applied — the notebook's character budget decides what reaches the answer.
88
+ */
48
89
  private findChunksByIdsOrdered;
90
+ /**
91
+ * Attaches `vector.similarity.cosine(chunk.embedding, $queryEmbedding)` to already
92
+ * hydrated chunks, by id, in JS.
93
+ *
94
+ * Why a separate read rather than one extra column on the hydration query: the
95
+ * hydration goes through `readMany`, which maps every row with the descriptor's
96
+ * generated mapper, and that mapper only ever reads the descriptor's own fields,
97
+ * computed fields and virtual fields (`define-entity.ts`). A projected column such
98
+ * as `score` is therefore silently DROPPED before the caller ever sees it. Reading
99
+ * the id/score pairs on their own and merging them here keeps entity mapping on
100
+ * `readMany` — this repository never hand-maps raw Neo4j records.
101
+ *
102
+ * SCOPE: this read scores only ids that the caller's own scope-gated query already
103
+ * returned, so it cannot widen the scope by construction.
104
+ */
105
+ private attachCosineScores;
49
106
  findParentName(params: {
50
107
  id: string;
51
108
  nodeType: string;
@@ -67,6 +124,35 @@ export declare class ChunkRepository implements OnModuleInit {
67
124
  chunkId: string;
68
125
  dataLimits?: DataLimits;
69
126
  }): Promise<Chunk>;
127
+ /**
128
+ * Hydrates many chunks in ONE query, with the same scope gate `findChunkById`
129
+ * applies to one. The caller previously looped with an `await` inside,
130
+ * costing one round trip per queued chunk. Returns only the chunks that
131
+ * exist and are in scope; the caller must not assume a 1:1 mapping with
132
+ * `chunkIds`.
133
+ *
134
+ * ORDER IS PART OF THE CONTRACT. `WHERE chunk.id IN $chunkIds` returns rows in
135
+ * whatever order the store yields them, but the loop this replaced hydrated in
136
+ * queue order, and that order reaches the contextualiser's per-chunk fan-out and
137
+ * the notebook entries it writes. Returning them shuffled changes what the answer
138
+ * node cites. The input order is therefore restored here, exactly as
139
+ * `findChunksByIdsOrdered` does for the retrieval path.
140
+ *
141
+ * `queryEmbedding` is OPTIONAL and, when given, attaches `score`: the cosine
142
+ * similarity of the chunk against the question. These chunks arrive from a fact
143
+ * join with no score of their own, which is precisely why an LLM used to have to
144
+ * judge them; cosine puts this half on the SAME scale as the document half, so the
145
+ * answer node can order both together. When it is absent the scoring clause is not
146
+ * in the Cypher at all. No floor is applied at any point: the notebook's character
147
+ * budget, not a threshold, decides what reaches the answer.
148
+ */
149
+ findChunksByIds(params: {
150
+ chunkIds: string[];
151
+ dataLimits?: DataLimits;
152
+ queryEmbedding?: number[];
153
+ }): Promise<Array<Chunk & {
154
+ score?: number;
155
+ }>>;
70
156
  /**
71
157
  * Pipeline hydration of a content node's chunks — deliberately WITHOUT `embedding`.
72
158
  *
@@ -1 +1 @@
1
- {"version":3,"file":"chunk.repository.d.ts","sourceRoot":"","sources":["../../../../src/foundations/chunk/repositories/chunk.repository.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"chunk.repository.d.ts","sourceRoot":"","sources":["../../../../src/foundations/chunk/repositories/chunk.repository.ts"],"names":[],"mappings":"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"}
@@ -27,6 +27,7 @@ const neo4j_service_1 = require("../../../core/neo4j/services/neo4j.service");
27
27
  const security_service_1 = require("../../../core/security/services/security.service");
28
28
  const chunk_entity_1 = require("../../chunk/entities/chunk.entity");
29
29
  const chunk_meta_1 = require("../entities/chunk.meta");
30
+ const retrieval_constants_1 = require("./retrieval.constants");
30
31
  const reciprocal_rank_fusion_1 = require("../services/reciprocal-rank-fusion");
31
32
  let ChunkRepository = ChunkRepository_1 = class ChunkRepository {
32
33
  constructor(neo4j, modelService, embedderService, clsService, securityService, aiSourceQuery, agentScope) {
@@ -101,67 +102,163 @@ let ChunkRepository = ChunkRepository_1 = class ChunkRepository {
101
102
  * lives with the caller (the contextualiser knows which content the run is
102
103
  * bound to), which is why it is passed down rather than derived here. Absent,
103
104
  * `EmbedderService.persistUsage` records nothing.
105
+ *
106
+ * Each returned chunk carries `score`: the cosine similarity of that chunk against
107
+ * the question embedding. Both retrieval halves have to reach the notebook on ONE
108
+ * scale — the answer node orders entries best-score-first and fills a character
109
+ * budget, so an unscored half sorts last however good it is. The RRF score is NOT
110
+ * usable for that: it is rank-derived (the top hit scores ≈1/61 whether it is a
111
+ * perfect match or noise) and is not comparable with a cosine from the graph half.
104
112
  */
105
113
  async findPotentialChunks(params) {
106
- const queryEmbedding = await this.embedderService.vectoriseText({
107
- text: params.question,
108
- attribution: params.attribution,
109
- });
114
+ // The question is embedded ONCE per turn. When the caller already has the
115
+ // vector (chunk_vector computes it and shares it through the graph state),
116
+ // do not pay for a second identical embedding.
117
+ const queryEmbedding = params.queryEmbedding ??
118
+ (await this.embedderService.vectoriseText({
119
+ text: params.question,
120
+ attribution: params.attribution,
121
+ }));
110
122
  // Lucene special-character escape so user questions can't break the fulltext query.
123
+ //
124
+ // NOT `buildFulltextTerm`: that helper tokenises and AND-joins a
125
+ // contains-wildcard per token, which is right for a search BOX (a few
126
+ // words) and wrong for a natural-language question. A 28-token question
127
+ // becomes 28 AND-ed clauses that no chunk can satisfy — measured against
128
+ // the eval corpus, the identical question went from 1,786 fulltext hits to
129
+ // 0, silently collapsing hybrid retrieval to its vector branch alone.
111
130
  const term = params.question.replace(/([+\-!(){}\[\]^"~*?:\\\/]|&&|\|\|)/g, "\\$1");
112
- // The access-scoped id-set both retrieval branches are filtered to.
113
131
  const scope = this.aiSourceQuery.build({
114
132
  dataLimits: params.dataLimits,
115
133
  currentUserId: this.clsService.get("userId"),
116
134
  securityService: this.securityService,
117
135
  returnsData: true,
118
136
  });
119
- // Both the vector and the lexical branch below are filtered to this id-set,
120
- // so confining it to the run's scope root confines the whole hybrid search.
121
137
  const scopeFilter = this.agentScope.build({ alias: "data", dataLimits: params.dataLimits });
122
- const scopeQuery = this.neo4j.initQuery();
123
- scopeQuery.queryParams = { ...scopeQuery.queryParams, ...scope.params, ...scopeFilter.params };
124
- scopeQuery.query += `
125
- ${scope.cypher}
126
- ${scopeFilter.cypher}
127
- MATCH (chunk:Chunk)<-[:HAS_CHUNK]-(data)
128
- RETURN COLLECT(DISTINCT chunk.id) AS chunkIds
129
- `;
130
- const scopeResult = await this.neo4j.read(scopeQuery.query, scopeQuery.queryParams);
131
- const chunkIds = scopeResult.records[0]?.get("chunkIds") ?? [];
132
- if (chunkIds.length === 0)
138
+ // How large is the scoped set? One cheap count decides which shape to use.
139
+ // Scope-first exact cosine has no recall cliff but reads every in-scope
140
+ // embedding; the index is cheaper at volume but can only post-filter a
141
+ // GLOBAL top-K, which is the cliff this task exists to remove.
142
+ const countQuery = this.neo4j.initQuery();
143
+ countQuery.queryParams = { ...countQuery.queryParams, ...scope.params, ...scopeFilter.params };
144
+ countQuery.query += `
145
+ ${scope.cypher}
146
+ ${scopeFilter.cypher}
147
+ MATCH (chunk:Chunk)<-[:HAS_CHUNK]-(data)
148
+ RETURN count(DISTINCT chunk) AS scopedCount
149
+ `;
150
+ const countResult = await this.neo4j.read(countQuery.query, countQuery.queryParams);
151
+ const scopedCount = Number(countResult.records[0]?.get("scopedCount") ?? 0);
152
+ if (scopedCount === 0)
133
153
  return [];
134
- const vectorResult = await this.neo4j.read(`
135
- CALL db.index.vector.queryNodes('chunks', 1000, $queryEmbedding)
136
- YIELD node AS candidateChunk, score
137
- WHERE candidateChunk.id IN $chunkIds
138
- RETURN candidateChunk.id AS id
139
- ORDER BY score DESC
140
- LIMIT 50
141
- `, { queryEmbedding, chunkIds });
142
- const vectorIds = vectorResult.records.map((record) => record.get("id"));
143
- let lexicalIds = [];
144
- if (term.trim()) {
145
- const lexicalResult = await this.neo4j.read(`
146
- CALL db.index.fulltext.queryNodes('chunk_content_search', $term)
147
- YIELD node, score
148
- WHERE node.id IN $chunkIds
149
- RETURN node.id AS id
150
- ORDER BY score DESC
151
- LIMIT 50
152
- `, { term, chunkIds });
153
- lexicalIds = lexicalResult.records.map((record) => record.get("id"));
154
- }
154
+ const vectorIds = scopedCount <= retrieval_constants_1.EXACT_SCAN_MAX_SCOPED_CHUNKS
155
+ ? await this.vectorIdsByExactScan({ scope, scopeFilter, queryEmbedding })
156
+ : await this.vectorIdsByIndex({ scope, scopeFilter, queryEmbedding });
157
+ const lexicalIds = term.trim() ? await this.lexicalIdsInScope({ scope, scopeFilter, term }) : [];
155
158
  const fusedIds = (0, reciprocal_rank_fusion_1.reciprocalRankFusion)([vectorIds, lexicalIds]).slice(0, 20);
156
- return this.findChunksByIdsOrdered(fusedIds);
159
+ return this.findChunksByIdsOrdered({ ids: fusedIds, queryEmbedding });
157
160
  }
158
- async findChunksByIdsOrdered(ids) {
159
- if (ids.length === 0)
161
+ /**
162
+ * Exact cosine over the scoped set. No recall cliff by construction: every
163
+ * in-scope chunk is scored, so a small tenant can never be crowded out of a
164
+ * global top-K by a large one.
165
+ */
166
+ async vectorIdsByExactScan(params) {
167
+ const query = this.neo4j.initQuery();
168
+ query.queryParams = {
169
+ ...query.queryParams,
170
+ ...params.scope.params,
171
+ ...params.scopeFilter.params,
172
+ queryEmbedding: params.queryEmbedding,
173
+ };
174
+ query.query += `
175
+ ${params.scope.cypher}
176
+ ${params.scopeFilter.cypher}
177
+ MATCH (chunk:Chunk)<-[:HAS_CHUNK]-(data)
178
+ WHERE chunk.embedding IS NOT NULL
179
+ WITH DISTINCT chunk
180
+ WITH chunk, vector.similarity.cosine(chunk.embedding, $queryEmbedding) AS score
181
+ WHERE score IS NOT NULL
182
+ RETURN chunk.id AS id
183
+ ORDER BY score DESC
184
+ LIMIT 50
185
+ `;
186
+ const result = await this.neo4j.read(query.query, query.queryParams);
187
+ return result.records.map((record) => record.get("id"));
188
+ }
189
+ /**
190
+ * Index-backed fallback for scoped sets too large to scan exactly. Still
191
+ * post-filters, so it still has a cliff — the over-fetch is what pushes that
192
+ * cliff out of reach, and it is deliberately far above the previous 1,000.
193
+ */
194
+ async vectorIdsByIndex(params) {
195
+ const query = this.neo4j.initQuery();
196
+ query.queryParams = {
197
+ ...query.queryParams,
198
+ ...params.scope.params,
199
+ ...params.scopeFilter.params,
200
+ queryEmbedding: params.queryEmbedding,
201
+ overFetch: retrieval_constants_1.CHUNK_VECTOR_OVERFETCH,
202
+ };
203
+ query.query += `
204
+ ${params.scope.cypher}
205
+ ${params.scopeFilter.cypher}
206
+ MATCH (chunk:Chunk)<-[:HAS_CHUNK]-(data)
207
+ WITH collect(DISTINCT chunk.id) AS scopedChunkIds
208
+ CALL db.index.vector.queryNodes('chunks', toInteger($overFetch), $queryEmbedding)
209
+ YIELD node AS candidateChunk, score
210
+ WITH scopedChunkIds, candidateChunk, score
211
+ WHERE candidateChunk.id IN scopedChunkIds
212
+ RETURN candidateChunk.id AS id
213
+ ORDER BY score DESC
214
+ LIMIT 50
215
+ `;
216
+ const result = await this.neo4j.read(query.query, query.queryParams);
217
+ return result.records.map((record) => record.get("id"));
218
+ }
219
+ /**
220
+ * Lexical branch, scoped the same way. Previously filtered against the
221
+ * client-side id list; now joined in the database like the vector branch.
222
+ */
223
+ async lexicalIdsInScope(params) {
224
+ const query = this.neo4j.initQuery();
225
+ query.queryParams = {
226
+ ...query.queryParams,
227
+ ...params.scope.params,
228
+ ...params.scopeFilter.params,
229
+ term: params.term,
230
+ };
231
+ query.query += `
232
+ ${params.scope.cypher}
233
+ ${params.scopeFilter.cypher}
234
+ MATCH (chunk:Chunk)<-[:HAS_CHUNK]-(data)
235
+ WITH collect(DISTINCT chunk.id) AS scopedChunkIds
236
+ CALL db.index.fulltext.queryNodes('chunk_content_search', $term)
237
+ YIELD node, score
238
+ WITH scopedChunkIds, node, score
239
+ WHERE node.id IN scopedChunkIds
240
+ RETURN node.id AS id
241
+ ORDER BY score DESC
242
+ LIMIT 50
243
+ `;
244
+ const result = await this.neo4j.read(query.query, query.queryParams);
245
+ return result.records.map((record) => record.get("id"));
246
+ }
247
+ /**
248
+ * INPUT ORDER IS PART OF THE CONTRACT here too: `WHERE chunk.id IN $ids` yields rows
249
+ * in store order, and the fused RRF order is what the caller means by "best first".
250
+ *
251
+ * `queryEmbedding` attaches the cosine score of each chunk against the question, on
252
+ * the same scale `findChunksByIds` puts on the graph half. No floor and no count cap
253
+ * are applied — the notebook's character budget decides what reaches the answer.
254
+ */
255
+ async findChunksByIdsOrdered(params) {
256
+ if (params.ids.length === 0)
160
257
  return [];
161
258
  const query = this.neo4j.initQuery({ serialiser: chunk_entity_1.ChunkDescriptor.model });
162
259
  query.queryParams = {
163
260
  ...query.queryParams,
164
- ids,
261
+ ids: params.ids,
165
262
  };
166
263
  query.query += `
167
264
  MATCH (chunk:Chunk)
@@ -169,8 +266,50 @@ let ChunkRepository = ChunkRepository_1 = class ChunkRepository {
169
266
  RETURN chunk
170
267
  `;
171
268
  const chunks = await this.neo4j.readMany(query);
172
- const byId = new Map(chunks.map((chunk) => [chunk.id, chunk]));
173
- return ids.map((id) => byId.get(id)).filter((chunk) => chunk !== undefined);
269
+ const scored = await this.attachCosineScores({ chunks, queryEmbedding: params.queryEmbedding });
270
+ const byId = new Map(scored.map((chunk) => [chunk.id, chunk]));
271
+ return params.ids
272
+ .map((id) => byId.get(id))
273
+ .filter((chunk) => chunk !== undefined);
274
+ }
275
+ /**
276
+ * Attaches `vector.similarity.cosine(chunk.embedding, $queryEmbedding)` to already
277
+ * hydrated chunks, by id, in JS.
278
+ *
279
+ * Why a separate read rather than one extra column on the hydration query: the
280
+ * hydration goes through `readMany`, which maps every row with the descriptor's
281
+ * generated mapper, and that mapper only ever reads the descriptor's own fields,
282
+ * computed fields and virtual fields (`define-entity.ts`). A projected column such
283
+ * as `score` is therefore silently DROPPED before the caller ever sees it. Reading
284
+ * the id/score pairs on their own and merging them here keeps entity mapping on
285
+ * `readMany` — this repository never hand-maps raw Neo4j records.
286
+ *
287
+ * SCOPE: this read scores only ids that the caller's own scope-gated query already
288
+ * returned, so it cannot widen the scope by construction.
289
+ */
290
+ async attachCosineScores(params) {
291
+ if (!params.queryEmbedding || params.chunks.length === 0)
292
+ return params.chunks;
293
+ const query = this.neo4j.initQuery();
294
+ query.queryParams = {
295
+ ...query.queryParams,
296
+ ids: params.chunks.map((chunk) => chunk.id),
297
+ queryEmbedding: params.queryEmbedding,
298
+ };
299
+ query.query += `
300
+ MATCH (chunk:Chunk)
301
+ WHERE chunk.id IN $ids AND chunk.embedding IS NOT NULL
302
+ RETURN chunk.id AS id, vector.similarity.cosine(chunk.embedding, $queryEmbedding) AS score
303
+ `;
304
+ const result = await this.neo4j.read(query.query, query.queryParams);
305
+ const scoreById = new Map();
306
+ for (const record of result.records) {
307
+ const id = record.get("id");
308
+ const score = record.get("score");
309
+ if (typeof id === "string" && typeof score === "number")
310
+ scoreById.set(id, score);
311
+ }
312
+ return params.chunks.map((chunk) => ({ ...chunk, score: scoreById.get(chunk.id) }));
174
313
  }
175
314
  async findParentName(params) {
176
315
  const query = this.neo4j.initQuery();
@@ -229,6 +368,51 @@ let ChunkRepository = ChunkRepository_1 = class ChunkRepository {
229
368
  `;
230
369
  return this.neo4j.readOne(query);
231
370
  }
371
+ /**
372
+ * Hydrates many chunks in ONE query, with the same scope gate `findChunkById`
373
+ * applies to one. The caller previously looped with an `await` inside,
374
+ * costing one round trip per queued chunk. Returns only the chunks that
375
+ * exist and are in scope; the caller must not assume a 1:1 mapping with
376
+ * `chunkIds`.
377
+ *
378
+ * ORDER IS PART OF THE CONTRACT. `WHERE chunk.id IN $chunkIds` returns rows in
379
+ * whatever order the store yields them, but the loop this replaced hydrated in
380
+ * queue order, and that order reaches the contextualiser's per-chunk fan-out and
381
+ * the notebook entries it writes. Returning them shuffled changes what the answer
382
+ * node cites. The input order is therefore restored here, exactly as
383
+ * `findChunksByIdsOrdered` does for the retrieval path.
384
+ *
385
+ * `queryEmbedding` is OPTIONAL and, when given, attaches `score`: the cosine
386
+ * similarity of the chunk against the question. These chunks arrive from a fact
387
+ * join with no score of their own, which is precisely why an LLM used to have to
388
+ * judge them; cosine puts this half on the SAME scale as the document half, so the
389
+ * answer node can order both together. When it is absent the scoring clause is not
390
+ * in the Cypher at all. No floor is applied at any point: the notebook's character
391
+ * budget, not a threshold, decides what reaches the answer.
392
+ */
393
+ async findChunksByIds(params) {
394
+ if (params.chunkIds.length === 0)
395
+ return [];
396
+ const query = this.neo4j.initQuery({ serialiser: chunk_entity_1.ChunkDescriptor.model });
397
+ const scopePredicate = this.agentScope.predicate({ alias: "data", dataLimits: params.dataLimits });
398
+ query.queryParams = {
399
+ ...query.queryParams,
400
+ chunkIds: params.chunkIds,
401
+ ...(scopePredicate?.params ?? {}),
402
+ };
403
+ query.query += `
404
+ MATCH (chunk:Chunk)
405
+ WHERE chunk.id IN $chunkIds
406
+ ${scopePredicate ? `AND EXISTS { MATCH (chunk)<-[:HAS_CHUNK]-(data) WHERE ${scopePredicate.cypher} }` : ""}
407
+ RETURN chunk
408
+ `;
409
+ const chunks = await this.neo4j.readMany(query);
410
+ const scored = await this.attachCosineScores({ chunks, queryEmbedding: params.queryEmbedding });
411
+ const byId = new Map(scored.map((chunk) => [chunk.id, chunk]));
412
+ return params.chunkIds
413
+ .map((id) => byId.get(id))
414
+ .filter((chunk) => chunk !== undefined);
415
+ }
232
416
  /**
233
417
  * Pipeline hydration of a content node's chunks — deliberately WITHOUT `embedding`.
234
418
  *
@@ -564,16 +748,21 @@ let ChunkRepository = ChunkRepository_1 = class ChunkRepository {
564
748
  if (params.chunkIds.length === 0)
565
749
  return [];
566
750
  const query = this.neo4j.initQuery();
567
- query.queryParams = { ...query.queryParams, chunkIds: params.chunkIds, window: params.window };
751
+ query.queryParams = { ...query.queryParams, chunkIds: params.chunkIds };
752
+ // Neo4j will not take a parameter as a variable-length bound, so the window
753
+ // is clamped to a small integer and interpolated. It is never user input —
754
+ // the only caller passes the NEIGHBOR_WINDOW constant — and Number() plus
755
+ // the clamp make injection impossible.
756
+ const window = Math.max(1, Math.min(5, Math.trunc(Number(params.window) || 1)));
568
757
  query.query = `
569
758
  UNWIND $chunkIds AS cid
570
759
  MATCH (c:Chunk {id: cid})
571
- OPTIONAL MATCH pBefore = (b:Chunk)-[:NEXT*1..]->(c)
760
+ OPTIONAL MATCH pBefore = (b:Chunk)-[:NEXT*1..${window}]->(c)
572
761
  WITH cid, c, b, length(pBefore) AS beforeDist ORDER BY beforeDist ASC
573
- WITH cid, c, [x IN collect(b) WHERE x IS NOT NULL][0..$window] AS befores
574
- OPTIONAL MATCH pAfter = (c)-[:NEXT*1..]->(a:Chunk)
762
+ WITH cid, c, [x IN collect(b) WHERE x IS NOT NULL][0..${window}] AS befores
763
+ OPTIONAL MATCH pAfter = (c)-[:NEXT*1..${window}]->(a:Chunk)
575
764
  WITH cid, befores, a, length(pAfter) AS afterDist ORDER BY afterDist ASC
576
- WITH cid, befores, [x IN collect(a) WHERE x IS NOT NULL][0..$window] AS afters
765
+ WITH cid, befores, [x IN collect(a) WHERE x IS NOT NULL][0..${window}] AS afters
577
766
  RETURN cid AS chunkId, [x IN befores | x.content] AS before, [x IN afters | x.content] AS after
578
767
  `;
579
768
  const result = await this.neo4j.read(query.query, query.queryParams);