@futdevpro/fdp-agent-memory 0.1.0 → 1.1.14

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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -7
  3. package/build/package.json +6 -5
  4. package/build/src/_cli/_collections/fam-arg.util.js +48 -0
  5. package/build/src/_cli/_collections/fam-cli.const.js +40 -0
  6. package/build/src/_cli/_collections/fam-output.util.js +86 -0
  7. package/build/src/_cli/_collections/fam-project-discovery.util.js +98 -0
  8. package/build/src/_cli/_commands/capture.command.js +73 -0
  9. package/build/src/_cli/_commands/config.command.js +93 -0
  10. package/build/src/_cli/_commands/doctor.command.js +124 -0
  11. package/build/src/_cli/_commands/errors.command.js +66 -0
  12. package/build/src/_cli/_commands/export.command.js +65 -0
  13. package/build/src/_cli/_commands/find-duplicates.command.js +97 -0
  14. package/build/src/_cli/_commands/import.command.js +136 -0
  15. package/build/src/_cli/_commands/init.command.js +147 -0
  16. package/build/src/_cli/_commands/read.command.js +109 -0
  17. package/build/src/_cli/_commands/scan-projects.command.js +138 -0
  18. package/build/src/_cli/_commands/scan.command.js +98 -0
  19. package/build/src/_cli/_commands/seed.command.js +40 -0
  20. package/build/src/_cli/_commands/serve.command.js +373 -0
  21. package/build/src/_cli/_commands/start.command.js +134 -0
  22. package/build/src/_cli/_commands/stats.command.js +54 -0
  23. package/build/src/_cli/_commands/write.command.js +103 -0
  24. package/build/src/_cli/_models/interfaces/fam-cli-global-options.interface.js +2 -0
  25. package/build/src/_cli/_models/interfaces/fam-cli-output.interface.js +9 -0
  26. package/build/src/_cli/_models/interfaces/fam-client-result.interface.js +2 -0
  27. package/build/src/_cli/_services/fam-client.service.js +140 -0
  28. package/build/src/_cli/register-commands.js +86 -0
  29. package/build/src/_collections/config-catalog.const.js +67 -1
  30. package/build/src/_collections/fam-console.util.js +367 -0
  31. package/build/src/_collections/fam-entry-bootstrap.util.js +158 -4
  32. package/build/src/_collections/fam-error-factory.util.js +0 -9
  33. package/build/src/_collections/fam-mcp-bridge.util.js +49 -0
  34. package/build/src/_collections/fam-reference-code.util.js +105 -0
  35. package/build/src/_collections/fam-version.const.js +10 -0
  36. package/build/src/_models/data-models/fam-entry-base-properties.const.js +1 -0
  37. package/build/src/_models/data-models/fam-entry.data-model.js +6 -0
  38. package/build/src/_models/data-models/fam-ingest-run.data-model.js +3 -1
  39. package/build/src/_models/data-models/fam-reference.data-model.js +7 -0
  40. package/build/src/_modules/capture/_collections/fam-capture.const.js +11 -0
  41. package/build/src/_modules/capture/_services/fam-auto-capture.control-service.js +87 -0
  42. package/build/src/_modules/capture/index.js +8 -0
  43. package/build/src/_modules/embedding/_collections/fam-embedding-prefix.util.js +77 -0
  44. package/build/src/_modules/embedding/_services/fam-duplicate-scan.control-service.js +202 -0
  45. package/build/src/_modules/embedding/_services/fam-embedding-pipeline.control-service.js +33 -9
  46. package/build/src/_modules/embedding/_services/fam-embedding.control-service.js +21 -2
  47. package/build/src/_modules/embedding/_services/fam-entry.data-service.js +135 -0
  48. package/build/src/_modules/embedding/_services/fam-vector-search.control-service.js +42 -32
  49. package/build/src/_modules/embedding/index.js +4 -1
  50. package/build/src/_modules/export/_collections/fam-export.const.js +22 -0
  51. package/build/src/_modules/export/_services/fam-export.control-service.js +64 -0
  52. package/build/src/_modules/export/index.js +8 -0
  53. package/build/src/_modules/ingest/_collections/fam-famignore.util.js +83 -0
  54. package/build/src/_modules/ingest/_collections/fam-file-routing.util.js +59 -48
  55. package/build/src/_modules/ingest/_collections/fam-git-repo.util.js +193 -0
  56. package/build/src/_modules/ingest/_collections/fam-project-identity.util.js +134 -0
  57. package/build/src/_modules/ingest/_collections/fam-scan-progress.util.js +57 -0
  58. package/build/src/_modules/ingest/_collections/fam-scan-summary.util.js +60 -0
  59. package/build/src/_modules/ingest/_collections/fam-scan-weight.util.js +53 -0
  60. package/build/src/_modules/ingest/_collections/fam-secret-exclude.util.js +37 -14
  61. package/build/src/_modules/ingest/_collections/fam-sliding-chunker.util.js +34 -0
  62. package/build/src/_modules/ingest/_collections/fam-ts-chunker.util.js +200 -14
  63. package/build/src/_modules/ingest/_services/fam-delta-compare.util.js +4 -1
  64. package/build/src/_modules/ingest/_services/fam-ingest-run.data-service.js +7 -4
  65. package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +349 -17
  66. package/build/src/_modules/ingest/_services/fam-scan.control-service.js +25 -2
  67. package/build/src/_modules/ingest/index.js +3 -1
  68. package/build/src/_modules/mcp/_collections/fam-active-rules.util.js +56 -0
  69. package/build/src/_modules/mcp/_collections/fam-core-tools.const.js +47 -6
  70. package/build/src/_modules/mcp/_services/fam-capabilities-tool.service.js +4 -4
  71. package/build/src/_modules/mcp/_services/fam-capability-registry.service.js +224 -18
  72. package/build/src/_modules/mcp/_services/fam-mcp-adapter.service.js +4 -4
  73. package/build/src/_modules/mcp/_services/fam-mcp-server.service.js +4 -4
  74. package/build/src/_modules/mcp/_services/fam-read-tool.service.js +53 -1
  75. package/build/src/_modules/mcp/_services/fam-write-tool.service.js +104 -8
  76. package/build/src/_modules/mcp/index.js +4 -4
  77. package/build/src/_modules/migration/_collections/fam-claude-mem-normalize.util.js +66 -3
  78. package/build/src/_modules/migration/_collections/fam-prompt-aggregate.util.js +143 -0
  79. package/build/src/_modules/migration/_collections/fam-target-mapping.util.js +19 -0
  80. package/build/src/_modules/migration/_enums/fam-claude-mem-source.type-enum.js +6 -0
  81. package/build/src/_modules/migration/_models/interfaces/fam-claude-mem.interface.js +5 -0
  82. package/build/src/_modules/migration/_services/fam-agent-memory-reader.service.js +125 -0
  83. package/build/src/_modules/migration/_services/fam-claude-mem-import.control-service.js +101 -18
  84. package/build/src/_modules/migration/_services/fam-import-dedup.data-service.js +53 -0
  85. package/build/src/_modules/migration/index.js +3 -1
  86. package/build/src/_modules/retrieval/_services/fam-retrieval-candidate.data-service.js +78 -4
  87. package/build/src/_modules/retrieval/_services/fam-retrieval.control-service.js +293 -50
  88. package/build/src/_modules/scope-reference/_collections/fam-scope-normalize.util.js +6 -3
  89. package/build/src/_modules/scope-reference/_services/fam-reference.data-service.js +18 -0
  90. package/build/src/_modules/scope-reference/_services/fam-scope-resolver.control-service.js +79 -20
  91. package/build/src/_routes/server/api/api.controller.js +34 -2
  92. package/build/src/_routes/server/client-app/client-app.control-service.js +1 -1
  93. package/build/src/_routes/server/server-status/server-status.controller.js +2 -1
  94. package/build/src/app.server.js +13 -1
  95. package/build/src/environments/environment.js +1 -1
  96. package/build/src/index.js +1 -1
  97. package/client-dist/{chunk-GHKRM4SM.js → chunk-I77GXVAQ.js} +1 -1
  98. package/client-dist/{chunk-LMTL7GA3.js → chunk-YXHWCJ5O.js} +1 -1
  99. package/client-dist/index.html +1 -1
  100. package/client-dist/{main-2KWB3QYK.js → main-PJPEDVJT.js} +1 -1
  101. package/package.json +6 -5
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_Retrieval_ControlService = void 0;
4
+ const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
5
+ const fam_reference_code_util_1 = require("../../../_collections/fam-reference-code.util");
4
6
  const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
5
7
  const embedding_1 = require("../../embedding");
6
8
  const scope_reference_1 = require("../../scope-reference");
@@ -78,12 +80,110 @@ class FAM_Retrieval_ControlService {
78
80
  const tableHits = await this.searchTable({ table, query, queryVector, scopeExpand, config, includeContent });
79
81
  allHits.push(...tableHits);
80
82
  }
81
- // weight-szorzó MÁR a searchTable-ben (finalScore). minScore-küszöb relevantSet.
82
- const relevantSet = allHits.filter((hit) => hit.finalScore >= config.minScore);
83
+ // ④b FEAT-003 determinisztikus kód-expanzió: ha a query azonosító-kódot tartalmaz, az ÖSSZES
84
+ // citáló elem maximális score-ral (exact citation) a vektor-zaj fölé kerül. Always-on, strictly
85
+ // additive (kód-mentes query-n no-op); a merge id szerint dedup-ol (vektor+kód ugyanazon elemen → felülemel).
86
+ const detectedCodes = fam_reference_code_util_1.FAM_ReferenceCode_Util.extract(query.query);
87
+ const mergedHits = await this.applyCodeExpansion({ query, detectedCodes, scopeExpand, vectorHits: allHits, includeContent });
88
+ // FEAT-006 context-injection deduper: a hívó által MÁR injektált `_id`-k kizárása (a minScore/topK ELŐTT,
89
+ // hogy ne foglaljanak helyet) — egy session ne kapja vissza ugyanazt a kontextust kétszer. A kizárás a
90
+ // kód-exact-re IS vonatkozik (ha a session már birtokolja, nem kell újra, F3-1 ellenére sem).
91
+ const visibleHits = this.excludeSeen(mergedHits, query.excludeIds);
92
+ // ⑤ weight-szorzó MÁR a hit-eken (finalScore). ⑥ minScore-küszöb → relevantSet — DE F3-2: a kód-exact
93
+ // citáció (`matchedCodes` nem-üres) BYPASS-olja a minScore-t (a determinisztikusan citált kód eleme
94
+ // releváns a score-tól függetlenül; a `weight≤0` továbbra is kizárt, mert az már a merge ELŐTT kiesik).
95
+ const relevantSet = visibleHits.filter((hit) => this.codeExactRank(hit) > 0 || hit.finalScore >= config.minScore);
83
96
  // ⑦ rendezés (finalScore desc → recency → _id asc).
84
97
  this.sortHits(relevantSet, config.scoreOrder);
85
98
  // ⑧ topK-vágás + dense-detektálás + suggestions.
86
- return this.buildResult({ query, relevantSet, config, scopeExpand });
99
+ return this.buildResult({ query, relevantSet, config, scopeExpand, detectedCodes });
100
+ }
101
+ // =========================================================================
102
+ // ④b KÓD-EXPANZIÓ (FEAT-003) — determinisztikus link-retrieval a vektor MELLÉ
103
+ // =========================================================================
104
+ /**
105
+ * A determinisztikus kód-expanzió (FEAT-003). A `detectedCodes` (a query-ből `FAM_ReferenceCode_Util`-lal
106
+ * felismert kódok) MINDEN cél-tárban `find({ referenceCodes: $in })`-nel behúzza az ÖSSZES citáló elemet
107
+ * (az AZONOS scope/tag/kind/active prefilterrel), `embeddingStatus`-tól FÜGGETLENÜL. A vektor-találatokkal
108
+ * id szerint merge-el (dedup): ha egy elem vektor-hit IS, a kód-citáció felülemeli a score-ját (exact >
109
+ * hasonlóság) + jelöli `matchedCodes`-szal. Kód-mentes query → a vektor-találatok változatlanul.
110
+ */
111
+ async applyCodeExpansion(set) {
112
+ if (!set.detectedCodes.length) {
113
+ return set.vectorHits;
114
+ }
115
+ // id→hit index a dedup-merge-hez (a vektor-találatok elsőként kerülnek be).
116
+ const byId = new Map();
117
+ for (const hit of set.vectorHits) {
118
+ byId.set(hit.id, hit);
119
+ }
120
+ for (const table of set.query.tables) {
121
+ const codeEntries = await new fam_retrieval_candidate_data_service_1.FAM_RetrievalCandidate_DataService().loadByReferenceCodes({
122
+ table: table,
123
+ scopeIdSet: set.scopeExpand.scopeIdSet,
124
+ tagFilter: set.query.tagFilter,
125
+ kindFilter: set.query.kindFilter,
126
+ }, set.detectedCodes);
127
+ for (const entry of codeEntries) {
128
+ this.mergeCodeHit({ byId, entry, table, detectedCodes: set.detectedCodes, includeContent: set.includeContent });
129
+ }
130
+ }
131
+ return Array.from(byId.values());
132
+ }
133
+ /**
134
+ * Egy kód-citáló entry beolvasztása a hit-halmazba (FEAT-003). `score=1.0` (exact citation = maximális
135
+ * relevancia), `finalScore=1.0×weight` (a meglévő weight-modellbe illesztve). `weight≤0` → kizárt
136
+ * (konzisztens a vektor-ággal). Ha az elem MÁR vektor-hit → a meglévő hit-et frissítjük (`matchedCodes`
137
+ * + a magasabb score felülír), egyébként új code-hit-et szúrunk.
138
+ */
139
+ mergeCodeHit(set) {
140
+ const id = set.entry._id;
141
+ if (!id) {
142
+ return;
143
+ }
144
+ const weight = set.entry.weight ?? 1;
145
+ if (weight <= 0) {
146
+ return;
147
+ }
148
+ const matched = (set.entry.referenceCodes ?? []).filter((code) => set.detectedCodes.includes(code));
149
+ if (!matched.length) {
150
+ return;
151
+ }
152
+ const codeFinalScore = 1.0 * weight;
153
+ const existing = set.byId.get(id);
154
+ if (existing) {
155
+ existing.matchedCodes = matched;
156
+ if (codeFinalScore > existing.finalScore) {
157
+ existing.score = 1.0;
158
+ existing.finalScore = codeFinalScore;
159
+ }
160
+ return;
161
+ }
162
+ set.byId.set(id, this.toCodeHit({ entry: set.entry, id, table: set.table, weight, matched, includeContent: set.includeContent }));
163
+ }
164
+ /** Egy kód-citáló entry → `FAM_RetrievalHit` (exact citation: `score=1.0`, `matchedCodes` kitöltve). */
165
+ toCodeHit(set) {
166
+ const lastModified = set.entry.__lastModified;
167
+ return {
168
+ id: set.id,
169
+ table: set.table,
170
+ score: 1.0,
171
+ weight: set.weight,
172
+ finalScore: 1.0 * set.weight,
173
+ content: set.includeContent ? set.entry.content : undefined,
174
+ kind: set.entry.kind,
175
+ tags: set.entry.tags ?? [],
176
+ ...this.ruleMeta(set.entry, set.table),
177
+ scopePath: (set.entry.scopePath ?? []),
178
+ source: set.entry.source,
179
+ sourceFilePath: set.entry.sourceFilePath ?? null,
180
+ absolutePath: set.entry.source?.absolutePath ?? null,
181
+ chunkType: set.entry.chunkType,
182
+ headingPath: set.entry.headingPath ?? [],
183
+ position: set.entry.position,
184
+ matchedCodes: set.matched,
185
+ lastModifiedMs: lastModified ? new Date(lastModified).getTime() : 0,
186
+ };
87
187
  }
88
188
  // =========================================================================
89
189
  // ②–④ PER-TÁR: prefilter → vektor-keresés → hit-row (SP-5.1/SP-5.2)
@@ -96,33 +196,65 @@ class FAM_Retrieval_ControlService {
96
196
  * dense-detektálás a topK ELŐTTI halmazon számol; dsgn-005 §4.1 / SP-5.2 feladat).
97
197
  */
98
198
  async searchTable(set) {
99
- // ①–② Mongo-prefilter jelölt-entry-k (a soft-delete + rules.isActive guard beépített).
100
- const candidates = await new fam_retrieval_candidate_data_service_1.FAM_RetrievalCandidate_DataService().loadCandidates({
101
- table: set.table,
102
- scopeIdSet: set.scopeExpand.scopeIdSet,
103
- tagFilter: set.query.tagFilter,
104
- kindFilter: set.query.kindFilter,
105
- });
106
- if (!candidates.length) {
107
- return [];
108
- }
109
- // A candidate-entry-k id→entry indexe (a hit-row + weight + recency összefűzéséhez).
199
+ // ①–② A jelölt-betöltés + ④ vektor-keresés — két út AZONOS EREDMÉNNYEL (a `byId` index a hit-row +
200
+ // weight + recency összefűzéséhez):
201
+ // • UNSCOPED (nincs scope/tag/kind szűrő, nem `rules`): a prefilter a TELJES tárat adná, ezért fölösleges
202
+ // mind a ~23k jelöltet betölteni — a candidateId-k = a teljes pool. Helyette közvetlenül a (hidratált)
203
+ // main-poolon keresünk (top-`MAX_PAGE_SIZE`), és CSAK a hit-ek metaadatát töltjük (`loadByIds`) → a
204
+ // Mongo-munka ~242×-ére csökken (mért: 727ms → 3ms). A pool = az összes aktív, completed jelölt, a
205
+ // weight a hit-en alkalmazva → BIT-AZONOS rangsor a régi (teljes-prefilter) úttal (`min(23k,500)=500`).
206
+ // • SCOPED (scope/tag/kind/`rules`): a Mongo-prefilter szűkít → candidateId-halmaz → szűkített keresés.
207
+ const isUnscoped = !set.scopeExpand.scopeIdSet
208
+ && !(set.query.tagFilter && set.query.tagFilter.length)
209
+ && !set.query.kindFilter
210
+ && set.table !== fam_table_type_enum_1.FAM_Table.rules;
110
211
  const byId = new Map();
111
- const candidateIds = [];
112
- for (const entry of candidates) {
113
- if (entry._id) {
114
- byId.set(entry._id, entry);
115
- candidateIds.push(entry._id);
212
+ let vectorHits;
213
+ if (isUnscoped) {
214
+ // ④ vektor-keresés a TELJES poolon (a vektor-réteg `limit`-je = `MAX_PAGE_SIZE` relevantSet a
215
+ // dense-detektáláshoz — NEM a végső topK).
216
+ vectorHits = await embedding_1.FAM_VectorSearch_ControlService.getInstance().search({
217
+ table: set.table,
218
+ queryVector: set.queryVector,
219
+ topK: MAX_PAGE_SIZE,
220
+ });
221
+ if (!vectorHits.length) {
222
+ return [];
223
+ }
224
+ const hitEntries = await new fam_retrieval_candidate_data_service_1.FAM_RetrievalCandidate_DataService()
225
+ .loadByIds(set.table, vectorHits.map((hit) => hit.id));
226
+ for (const entry of hitEntries) {
227
+ if (entry._id) {
228
+ byId.set(entry._id, entry);
229
+ }
116
230
  }
117
231
  }
118
- // ④ vektor-keresés a szűkített poolon (koszinusz; a vektor-réteg `limit`-je = a teljes
119
- // jelölt-halmaz, hogy a teljes relevantSet meglegyen a dense-detektáláshoz NEM a végső topK).
120
- const vectorHits = await embedding_1.FAM_VectorSearch_ControlService.getInstance().search({
121
- table: set.table,
122
- queryVector: set.queryVector,
123
- candidateIds: candidateIds,
124
- topK: Math.min(candidateIds.length, MAX_PAGE_SIZE),
125
- });
232
+ else {
233
+ // ①–② Mongo-prefilter jelölt-entry-k (a soft-delete + rules.isActive guard beépített).
234
+ const candidates = await new fam_retrieval_candidate_data_service_1.FAM_RetrievalCandidate_DataService().loadCandidates({
235
+ table: set.table,
236
+ scopeIdSet: set.scopeExpand.scopeIdSet,
237
+ tagFilter: set.query.tagFilter,
238
+ kindFilter: set.query.kindFilter,
239
+ });
240
+ if (!candidates.length) {
241
+ return [];
242
+ }
243
+ const candidateIds = [];
244
+ for (const entry of candidates) {
245
+ if (entry._id) {
246
+ byId.set(entry._id, entry);
247
+ candidateIds.push(entry._id);
248
+ }
249
+ }
250
+ // ④ vektor-keresés a szűkített poolon (a relevantSet a dense-detektáláshoz, NEM a végső topK).
251
+ vectorHits = await embedding_1.FAM_VectorSearch_ControlService.getInstance().search({
252
+ table: set.table,
253
+ queryVector: set.queryVector,
254
+ candidateIds: candidateIds,
255
+ topK: Math.min(candidateIds.length, MAX_PAGE_SIZE),
256
+ });
257
+ }
126
258
  // ⑤ weight-szorzó: finalScore = score × (weight ?? 1); weight ≤ 0 → kizárt (nem kerül a hits közé).
127
259
  const hits = [];
128
260
  for (const vectorHit of vectorHits) {
@@ -134,28 +266,66 @@ class FAM_Retrieval_ControlService {
134
266
  if (weight <= 0) {
135
267
  continue;
136
268
  }
137
- hits.push(this.toHit({ entry, vectorHit, table: set.table, weight, includeContent: set.includeContent }));
269
+ hits.push(this.toHit({
270
+ entry, vectorHit, table: set.table, weight,
271
+ importChunkWeight: set.config.importChunkWeight, includeContent: set.includeContent,
272
+ }));
138
273
  }
139
274
  return hits;
140
275
  }
276
+ /**
277
+ * Az import-only chunkok (`chunkType:"imports"`) read-idejű rangsor-szorzója (FAM-REV-060). Egy import-sor
278
+ * (`import { X } from "./y"`) magas cosine-t kap egy szimbólum-név query-re, de alacsony az ÉRTÉKe (csak
279
+ * import, nem definíció) → `importChunkWeight` (<1) a definíció-chunkok ALÁ rangsorolja. Minden más chunkType
280
+ * (class/function/interface/generic/…) érintetlen (×1). READ-time, a nyers `score` (relevancia) változatlan.
281
+ */
282
+ chunkTypeFactor(chunkType, importChunkWeight) {
283
+ return chunkType === 'imports' ? importChunkWeight : 1;
284
+ }
141
285
  /** Egy candidate-entry + a vektor-hit → `FAM_RetrievalHit` (finalScore + denormalizált mezők). */
142
286
  toHit(set) {
143
287
  const lastModified = set.entry.__lastModified;
288
+ // ⑤ weight-szorzó + FAM-REV-060 import-chunk read-faktor: finalScore = score × weight × chunkTypeFactor.
289
+ const chunkFactor = this.chunkTypeFactor(set.entry.chunkType, set.importChunkWeight);
144
290
  return {
145
291
  id: set.vectorHit.id,
146
292
  table: set.table,
147
293
  score: set.vectorHit.score,
148
294
  weight: set.weight,
149
- finalScore: set.vectorHit.score * set.weight,
295
+ finalScore: set.vectorHit.score * set.weight * chunkFactor,
150
296
  content: set.includeContent ? set.entry.content : undefined,
151
297
  kind: set.entry.kind,
152
298
  tags: set.entry.tags ?? [],
299
+ ...this.ruleMeta(set.entry, set.table),
153
300
  scopePath: (set.entry.scopePath ?? []),
154
301
  source: set.entry.source,
155
302
  sourceFilePath: set.entry.sourceFilePath ?? null,
303
+ absolutePath: set.entry.source?.absolutePath ?? null,
304
+ // Location-infó (FAM-REV-049): chunk-típus + symbol-lánc + sor/char-pozíció — hogy keresésből
305
+ // rögtön „melyik fájl / melyik symbol / hányadik sor" legyen, ne csak a fájlnév.
306
+ chunkType: set.entry.chunkType,
307
+ headingPath: set.entry.headingPath ?? [],
308
+ position: set.entry.position,
156
309
  lastModifiedMs: lastModified ? new Date(lastModified).getTime() : 0,
157
310
  };
158
311
  }
312
+ /**
313
+ * A `rules` tár per-tár metaadatai a hit-en (`category`/`isActive`/`applicableScopes`) — FEAT-005/006 P1:
314
+ * a zóna-alapú aktív-rule kezelés alapja, hogy a hívó/hook a keresési hit-ből is lássa, MI aktív + mely
315
+ * zónában. MÁS táraknál üres objektum (e mezők a `FAM_Entry` bázison nincsenek; a `rules`-doc-on igen —
316
+ * a loose `Partial<FAM_Rules_DataModel>` view a meglévő `as`-konvenció szerint, mint a `source`/`scopePath`).
317
+ */
318
+ ruleMeta(entry, table) {
319
+ if (table !== fam_table_type_enum_1.FAM_Table.rules) {
320
+ return {};
321
+ }
322
+ const rule = entry;
323
+ return {
324
+ category: rule.category,
325
+ isActive: rule.isActive,
326
+ applicableScopes: (rule.applicableScopes ?? []),
327
+ };
328
+ }
159
329
  // =========================================================================
160
330
  // ⑦ RENDEZÉS (SP-5.2, dsgn-005 §5)
161
331
  // =========================================================================
@@ -167,6 +337,14 @@ class FAM_Retrieval_ControlService {
167
337
  */
168
338
  sortHits(hits, scoreOrder) {
169
339
  hits.sort((a, b) => {
340
+ // F3-1 (garantált kód-exact inclusion): a determinisztikus kód-citáció (`matchedCodes` nem-üres) a
341
+ // legkülső rendezési kulcs — a fuzzy vektor-hasonlóság ELÉ rangsorol (decisiveness). Az
342
+ // `effectiveTopK = max(topK, #codeExact)`-kal együtt ez GARANTÁLJA, hogy a citált kód MINDEN eleme
343
+ // visszakerül (a topK sosem vágja le). Kód-mentes query-n nincs code-exact hit → változatlan rangsor.
344
+ const codePriority = this.codeExactRank(b) - this.codeExactRank(a);
345
+ if (codePriority !== 0) {
346
+ return codePriority;
347
+ }
170
348
  const primary = this.primaryComparator(a, b, scoreOrder);
171
349
  if (primary !== 0) {
172
350
  return primary;
@@ -190,36 +368,58 @@ class FAM_Retrieval_ControlService {
190
368
  // 'weighted' (default).
191
369
  return b.finalScore - a.finalScore;
192
370
  }
371
+ /**
372
+ * F3-1: egy hit **kód-exact**-e (van legalább egy `matchedCodes` determinisztikus citáció)? Rendezési kulcs
373
+ * (1 = kód-exact → előre, 0 = pusztán vektor-hit) és a garantált-inclusion `effectiveTopK` számlálója.
374
+ */
375
+ codeExactRank(hit) {
376
+ return hit.matchedCodes && hit.matchedCodes.length ? 1 : 0;
377
+ }
378
+ /**
379
+ * FEAT-006 context-injection deduper: a hívó által MÁR injektált (`excludeIds`) bejegyzések kizárása a
380
+ * találatokból. Üres/hiányzó lista → változatlan halmaz (nincs fölös allokáció). Állapotmentes: a
381
+ * „már látott" halmazt a hívó (session/hook) tartja.
382
+ */
383
+ excludeSeen(hits, excludeIds) {
384
+ if (!excludeIds || !excludeIds.length) {
385
+ return hits;
386
+ }
387
+ const seen = new Set(excludeIds);
388
+ return hits.filter((hit) => !seen.has(hit.id));
389
+ }
193
390
  // =========================================================================
194
391
  // ⑧ topK-vágás + DENSE-DETEKTÁLÁS + signaling (SP-5.3, dsgn-005 §4)
195
392
  // =========================================================================
196
393
  /**
197
- * A rendezett relevantSet → `FAM_RetrievalResult` (dsgn-005 §4). `totalRelevant = relevantSet.length`;
198
- * `returnedHits` = az első topK; `truncated = totalRelevant > topK`. Dense dual-gate (§4.2):
199
- * `(totalRelevant >= ratio × topK) || (totalRelevant >= absMin)`, DE csak `truncated:true` mellett
200
- * (a kötés: vágás nélkül nincs elrejtett releváns tömeg). A suggestions a relevantSet valós
201
- * statisztikáiból (§4.4). Az `uncertaintyNotes` a scope-prefilter jegyzetei (dsgn-002 §4).
394
+ * A rendezett relevantSet → `FAM_RetrievalResult` (dsgn-005 §4) **confidence-padlóval** (FAM-REV-054,
395
+ * 2026-06-20 dogfood). A relevancia-JELZÉSEK (`totalRelevant`/`denseResults`/`truncated`/`suggestions`)
396
+ * a MAGABIZTOSAN releváns halmazból (`score >= relevanceFloor`, nyers cosine-on, hogy a weight ne torzítson)
397
+ * számolnak NEM a `minScore=0` melletti teljes poolból (ami minden cosine>0 elemet „relevánsnak" vett,
398
+ * így minden query-n hamis „dense + N=pool" + „emeld a topK-t a pool-ra" jött). A `returnedHits` viszont
399
+ * best-effort marad (recall): a hívó megkapja a topK legjobbat, DE ha egyik sem magabiztos, a jelek
400
+ * ŐSZINTÉN mutatják (north-star: „ha valami hiányzik, MEGMONDJA" — `confidentSet` üres → low-confidence note).
401
+ * Dense dual-gate (§4.2): `(N >= ratio×topK) || (N >= absMin)`, csak `truncated:true` mellett. Az
402
+ * `uncertaintyNotes` a scope-prefilter jegyzetei (dsgn-002 §4). **Spec-drift:** dsgn-005 §4.1 jelenleg
403
+ * `totalRelevant = relevantSet.length`-et ír; ez a confidence-floor-os finomítás (a katalógus + REVIEW-FINDINGS jelöli).
202
404
  */
203
405
  buildResult(set) {
204
- const totalRelevant = set.relevantSet.length;
205
- const effectiveTopK = set.config.topK;
406
+ // F3-1 (garantált kód-exact inclusion): az effektív topK a kért `topK` ÉS a kód-exact citációk
407
+ // száma közül a nagyobb — így a determinisztikusan citált kód MINDEN eleme visszakerül, akkor is, ha
408
+ // többen vannak, mint a kért `topK` (a `sortHits` ezeket a halmaz elejére rendezte). Kód-mentes
409
+ // query-n `#codeExact = 0` → `effectiveTopK = topK` (változatlan viselkedés).
410
+ const codeExactCount = set.relevantSet.filter((hit) => this.codeExactRank(hit)).length;
411
+ const effectiveTopK = Math.max(set.config.topK, codeExactCount);
206
412
  const returnedHits = set.relevantSet.slice(0, effectiveTopK);
413
+ // Magabiztosan releváns halmaz: a nyers cosine `score` (NEM finalScore — a weight ranking-boost, nem
414
+ // relevancia) a `relevanceFloor` felett. A relevantSet rendezett, ezért a confidentSet is.
415
+ const confidentSet = set.relevantSet.filter((hit) => hit.score >= set.config.relevanceFloor);
416
+ const totalRelevant = confidentSet.length;
207
417
  const truncated = totalRelevant > effectiveTopK;
208
- // Dual-gate dense (§4.2) — csak `truncated:true` mellett lehet `true`.
418
+ // Dual-gate dense (§4.2) — a CONFIDENT halmazon, csak `truncated:true` mellett.
209
419
  const densenessRatio = totalRelevant / effectiveTopK;
210
420
  const denseByRatio = densenessRatio >= set.config.denseResultRatio;
211
421
  const denseByAbs = totalRelevant >= set.config.denseResultAbsMin;
212
422
  const denseResults = truncated && (denseByRatio || denseByAbs);
213
- // Suggestions (§4.4) — a relevantSet valós statisztikáiból + a query paramétereiből.
214
- const suggestions = (denseResults || truncated)
215
- ? fam_retrieval_suggestions_util_1.FAM_RetrievalSuggestions_Util.generate({
216
- relevantSet: set.relevantSet,
217
- query: set.query,
218
- effectiveTopK: effectiveTopK,
219
- truncated: truncated,
220
- maxPageSize: MAX_PAGE_SIZE,
221
- })
222
- : [];
223
423
  return {
224
424
  query: set.query.query,
225
425
  tables: set.query.tables,
@@ -227,10 +427,45 @@ class FAM_Retrieval_ControlService {
227
427
  denseResults: denseResults,
228
428
  totalRelevant: totalRelevant,
229
429
  truncated: truncated,
230
- suggestions: suggestions,
430
+ suggestions: this.buildSuggestions({
431
+ relevantSet: set.relevantSet,
432
+ confidentSet: confidentSet,
433
+ query: set.query,
434
+ effectiveTopK: effectiveTopK,
435
+ truncated: truncated,
436
+ relevanceFloor: set.config.relevanceFloor,
437
+ }),
231
438
  uncertaintyNotes: set.scopeExpand.uncertaintyNotes,
439
+ detectedCodes: set.detectedCodes,
232
440
  };
233
441
  }
442
+ /**
443
+ * A suggestions-lista (FAM-REV-054). HA nincs magabiztos találat, de adtunk vissza best-effort hit-et →
444
+ * ŐSZINTE low-confidence jelzés (north-star: „megmondja, ha valószínűleg nincs lefedve") a nyers top-score-ral.
445
+ * Egyébként a szűkítési javaslatok a CONFIDENT halmaz statisztikáiból (nem a teljes zaj-poolból), és csak
446
+ * ha `truncated` (van a topK-n túli magabiztos elem). Kód-citáció (`score=1.0`) mindig magabiztos.
447
+ */
448
+ buildSuggestions(set) {
449
+ if (!set.confidentSet.length) {
450
+ if (!set.relevantSet.length) {
451
+ return [];
452
+ }
453
+ const topScore = set.relevantSet[0].score;
454
+ return [`⚠️ nincs magabiztosan releváns találat (a legjobb score ${topScore.toFixed(2)} < `
455
+ + `relevanceFloor ${set.relevanceFloor.toFixed(2)}) — lehet, hogy ez nincs lefedve; `
456
+ + `fogalmazd át a query-t, vagy tágítsd/váltsd a scope-ot/tárat`];
457
+ }
458
+ if (!set.truncated) {
459
+ return [];
460
+ }
461
+ return fam_retrieval_suggestions_util_1.FAM_RetrievalSuggestions_Util.generate({
462
+ relevantSet: set.confidentSet,
463
+ query: set.query,
464
+ effectiveTopK: set.effectiveTopK,
465
+ truncated: set.truncated,
466
+ maxPageSize: MAX_PAGE_SIZE,
467
+ });
468
+ }
234
469
  // =========================================================================
235
470
  // CONFIG-FELOLDÁS (SP-5.2, dsgn-005 §6 / dsgn-007 SSOT) + query-embedding (SP-5.2 ④)
236
471
  // =========================================================================
@@ -244,9 +479,15 @@ class FAM_Retrieval_ControlService {
244
479
  async resolveReadConfig(table, query) {
245
480
  const config = config_control_service_1.FAM_Config_ControlService.getInstance();
246
481
  const ctx = { table: table };
247
- // query.topK / query.minScore felülírja a config-default-ot (dsgn-003 §2.1 query-opció).
248
- const topKDefault = query.topK ?? await this.resolveNumber(config, 'read.topK', ctx, 5);
482
+ // query.topK / query.minScore felülírja a config-default-ot (dsgn-003 §2.1 query-opció). A `depth:'deep'`
483
+ // (mély feltárás) az alap-topK-t a `read.deepTopK`-ra emeli (sokkal több chunk) — az explicit `topK` nyer.
484
+ const isDeep = query.depth === 'deep';
485
+ const topKDefault = query.topK ?? (isDeep
486
+ ? await this.resolveNumber(config, 'read.deepTopK', ctx, 25)
487
+ : await this.resolveNumber(config, 'read.topK', ctx, 5));
249
488
  const minScore = query.minScore ?? await this.resolveNumber(config, 'read.minScore', ctx, 0);
489
+ const relevanceFloor = await this.resolveNumber(config, 'read.relevanceFloor', ctx, 0.5);
490
+ const importChunkWeight = await this.resolveNumber(config, 'read.importChunkWeight', ctx, 0.4);
250
491
  const denseResultRatio = await this.resolveNumber(config, 'read.denseResultRatio', ctx, 2.0);
251
492
  const denseResultAbsMin = await this.resolveNumber(config, 'read.denseResultAbsMin', ctx, 25);
252
493
  const scoreOrderValue = (await config.resolve('read.scoreOrder', ctx)).value;
@@ -254,6 +495,8 @@ class FAM_Retrieval_ControlService {
254
495
  return {
255
496
  topK: Math.min(topKDefault, MAX_PAGE_SIZE),
256
497
  minScore: minScore,
498
+ relevanceFloor: relevanceFloor,
499
+ importChunkWeight: importChunkWeight,
257
500
  denseResultRatio: denseResultRatio,
258
501
  denseResultAbsMin: denseResultAbsMin,
259
502
  scoreOrder: scoreOrder,
@@ -20,8 +20,11 @@ class FAM_ScopeNormalize_Util {
20
20
  /**
21
21
  * Egy nyers token normalizálása a de-dup / exact-match kulcshoz (dsgn-002 §3.1). A lépések:
22
22
  * (1) lowercase (Unicode-aware), (2) ékezet-fold (NFD + diakritika-strip — `"Adventör"` →
23
- * `"adventor"`), (3) belső whitespace-collapse egyetlen szóközre, (4) trim. Üres/nem-string
24
- * bemenet üres string (a hívó dönti el, hogy az üres kulcs match-elhet-e).
23
+ * `"adventor"`), (3) **szeparátor-egységesítés** (FAM-REV-042): minden whitespace / `-` / `_`
24
+ * futam egyetlen szóközre így `"auth-service"` `"auth service"` `"auth_service"` (a
25
+ * kötőjel-vs-alulvonás-vs-szóköz variánsok NEM hoznak létre dupla scope-ot), (4) trim. Üres/nem-string
26
+ * bemenet → üres string (a hívó dönti el, hogy az üres kulcs match-elhet-e). A tárolt `canonicalName`
27
+ * az EREDETI marad — a match futásidőben mindkét oldalt normalizálja, így nincs adatmigráció.
25
28
  */
26
29
  static normalize(raw) {
27
30
  if (!raw) {
@@ -31,7 +34,7 @@ class FAM_ScopeNormalize_Util {
31
34
  .toLowerCase()
32
35
  .normalize('NFD')
33
36
  .replace(FAM_ScopeNormalize_Util.DIACRITICS, '')
34
- .replace(/\s+/g, ' ')
37
+ .replace(/[\s_-]+/g, ' ')
35
38
  .trim();
36
39
  }
37
40
  /**
@@ -62,6 +62,10 @@ class FAM_Reference_DataService extends nts_dynamo_1.DyNTS_DataService {
62
62
  return null;
63
63
  }
64
64
  for (const reference of references) {
65
+ // Negatív-alias veto (disambiguáció): ez a reference EXPLICIT nem oldja fel a tokent → kihagyjuk.
66
+ if (FAM_Reference_DataService.vetoesToken(targetKey, reference)) {
67
+ continue;
68
+ }
65
69
  if (fam_scope_normalize_util_1.FAM_ScopeNormalize_Util.normalize(reference.canonicalTerm) === targetKey) {
66
70
  return reference;
67
71
  }
@@ -74,6 +78,20 @@ class FAM_Reference_DataService extends nts_dynamo_1.DyNTS_DataService {
74
78
  }
75
79
  return null;
76
80
  }
81
+ /**
82
+ * Veto-zza-e a reference a tokent (negatív-alias, „ne keverjünk dolgokat"): a token (normalizáltan) egyezik-e
83
+ * a reference bármely `negativeAliases`-ével. A tokent MAGA normalizálja (a `FAM_ScopeNormalize_Util` choke-
84
+ * ponton — idempotens, ha a hívó már normalizált adott át) → szeparátor-/ékezet-/kis-nagybetű-robusztus. Ha
85
+ * igen, ez a reference NEM kandidátus a tokenre (exact + fuzzy + vektor egyaránt). Üres negatív-lista → false.
86
+ */
87
+ static vetoesToken(token, reference) {
88
+ const target = fam_scope_normalize_util_1.FAM_ScopeNormalize_Util.normalize(token);
89
+ if (!target.length) {
90
+ return false;
91
+ }
92
+ return (reference.negativeAliases ?? [])
93
+ .some((negative) => fam_scope_normalize_util_1.FAM_ScopeNormalize_Util.normalize(negative) === target);
94
+ }
77
95
  // =========================================================================
78
96
  // STRING-FUZZY (dsgn-002 §3.1, SP-3.2)
79
97
  // =========================================================================