@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
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_ReferenceCode_Util = void 0;
4
+ /**
5
+ * `FAM_ReferenceCode_Util` (FEAT-003) — azonosító-kód felismerő + normalizáló. **Pure** (dependency-
6
+ * mentes statikus util — FDP-konvenció: nincs top-level export-function). KÖZÖS choke-pont a write-
7
+ * (inverted-index építés) és a read- (determinisztikus expanzió) oldalon: az egyezés-garancia az, hogy
8
+ * MINDKÉT oldal UGYANEZT a felismerőt + normalizálást használja.
9
+ *
10
+ * Cél: requirement-/feature-/jegy-azonosító kódok (`REQ-AUTH-001`, `FEAT-001`, `BFR-AM-012`, `JIRA-1234`,
11
+ * `MP-5`, `SP-5.2`, `CVE-2021-1234`, …) kinyerése tetszőleges szövegből, normalizált (uppercase) alakban,
12
+ * dedup-olva. A vektor-keresés a kódokra gyenge (a parafrázisok zajba olvadnak) — a felismert kód EXPLICIT
13
+ * link-kulccsá válik (FEAT-003 §3). A FEAT-005 trigger-engine `regex`/`reference` triggerMode-ja is ezt használja.
14
+ */
15
+ class FAM_ReferenceCode_Util {
16
+ /**
17
+ * A default felismerő-minta (generikus, magas-precízió — OSS-semleges, NEM hardkódol projekt-konvenciót).
18
+ * Nagybetűs prefix (≥2 kar) + 0+ `-<alnum>` szegmens + **numerikus farok** (`-<számjegyek>`, opc. `.<számjegyek>`):
19
+ * `REQ-AUTH-001` / `FEAT-001` / `ADR-004` / `BFR-AM-012` / `JIRA-1234` / `GH-123` / `MP-5` / `SP-5.2` /
20
+ * `FAM-REV-049` / `CVE-2021-1234`. A numerikus-farok kötöttség zárja ki a prózát; a `g`-flag a több-
21
+ * találatot. **Case-INSENSITIVE (`i`):** a kódok case-független azonosítók (`REQ-AUTH-001` ≡ `req-auth-001`)
22
+ * — a `normalize` uppercase-re hozza, hogy a write/read egyezzen. A precíziót a denylist őrzi (lent).
23
+ */
24
+ static DEFAULT_PATTERN = /\b[A-Z][A-Z0-9]+(?:-[A-Z0-9]+)*-\d+(?:\.\d+)*\b/gi;
25
+ /**
26
+ * A FALSE-positive prefix-denylist (UPPERCASE-normalizált prefixre szűr — az ELSŐ `-` előtti szegmens):
27
+ * (a) kódolás-/szabvány-/termék-prefixek (`UTF-8`, `SHA-256`, `ISO-8601`, `AES-256`, `GPT-4`), (b) a
28
+ * case-insensitive matching miatt a gyakori lowercase-próza minták (`page-12`, `line-23`, `item-5`,
29
+ * `section-2`). Per-projekt felülírható (future config); a default konzervatív (a valódi REQ-/FEAT-/jegy-
30
+ * kódokat NEM érinti).
31
+ */
32
+ static PREFIX_DENYLIST = new Set([
33
+ // (a) kódolás / szabvány / termék
34
+ 'UTF', 'UTF8', 'SHA', 'SHA1', 'SHA256', 'MD5', 'BASE', 'BASE64', 'ISO', 'AES', 'RSA',
35
+ 'IPV', 'IPV4', 'IPV6', 'ES', 'ECMA', 'COVID', 'GPT', 'HTTP', 'TLS',
36
+ // (b) gyakori lowercase-próza (a case-insensitive matching zaj-csökkentése)
37
+ 'PAGE', 'LINE', 'ITEM', 'STEP', 'FIG', 'NOTE', 'SECTION', 'CHAPTER', 'PART', 'ROW',
38
+ 'COL', 'VOL', 'VER', 'VERSION', 'NUM', 'TABLE', 'APPENDIX',
39
+ // (c) Tailwind/CSS utility-prefixek (2026-06-20 audit false-positive: `bg-neutral-900`→BG, `py-1`→PY)
40
+ // — a `≥2`-karakteres prefixek (a minta `[A-Z][A-Z0-9]+` ≥2-t igényel; az 1-betűs `p-`/`m-`/`z-` nem match).
41
+ 'BG', 'TEXT', 'PX', 'PY', 'PT', 'PB', 'PL', 'PR', 'MX', 'MY', 'MT', 'MB', 'ML', 'MR',
42
+ 'GAP', 'SPACE', 'BORDER', 'RING', 'INSET', 'OPACITY', 'DIVIDE', 'LEADING', 'TRACKING',
43
+ 'GRID', 'FLEX', 'ORDER', 'BASIS', 'GROW', 'SHRINK', 'TOP', 'BOTTOM', 'LEFT', 'RIGHT',
44
+ 'MIN', 'MAX', 'ASPECT', 'SCALE', 'ROTATE', 'TRANSLATE', 'SKEW', 'DURATION', 'DELAY',
45
+ 'INDENT', 'FILL', 'STROKE', 'OUTLINE', 'DECORATION',
46
+ ]);
47
+ /** Egy felismert kód max-hossza (pathológikus match elleni guard). */
48
+ static MAX_CODE_LENGTH = 64;
49
+ /** Egy szövegből kinyert kódok max-száma (DoS-guard nagy tartalomra). */
50
+ static MAX_CODES_PER_TEXT = 100;
51
+ /**
52
+ * Egy szöveg összes felismert kódja — **normalizált (uppercase), dedup-olt** listában (felismerés-sorrend).
53
+ * Üres / kód-mentes szöveg → üres tömb (a hívó ebből „nincs kód"-ot lát). Az `extraPatterns` opcionális
54
+ * per-projekt minták (a default MELLETT futnak); a denylist + normalizálás + dedup rájuk is alkalmazódik.
55
+ */
56
+ static extract(text, extraPatterns) {
57
+ if (typeof text !== 'string' || !text.length) {
58
+ return [];
59
+ }
60
+ const patterns = [FAM_ReferenceCode_Util.DEFAULT_PATTERN, ...(extraPatterns ?? [])];
61
+ const seen = new Set();
62
+ const result = [];
63
+ for (const pattern of patterns) {
64
+ // Friss lastIndex minden hívásnál (a `g`-flagű minta state-es; külön matchAll-lal izoláljuk).
65
+ const matches = text.matchAll(FAM_ReferenceCode_Util.global(pattern));
66
+ for (const match of matches) {
67
+ const candidate = FAM_ReferenceCode_Util.normalize(match[0]);
68
+ if (!FAM_ReferenceCode_Util.isAcceptable(candidate, seen)) {
69
+ continue;
70
+ }
71
+ seen.add(candidate);
72
+ result.push(candidate);
73
+ if (result.length >= FAM_ReferenceCode_Util.MAX_CODES_PER_TEXT) {
74
+ return result;
75
+ }
76
+ }
77
+ }
78
+ return result;
79
+ }
80
+ /** `true`, ha a szöveg TARTALMAZ legalább egy felismerhető kódot (a `read` gyors elágazásához). */
81
+ static containsCode(text, extraPatterns) {
82
+ return FAM_ReferenceCode_Util.extract(text, extraPatterns).length > 0;
83
+ }
84
+ /** Egy kód normalizálása (uppercase + trim) — a write/read egyezés-garanciájához (case-insensitive). */
85
+ static normalize(code) {
86
+ return code.trim().toUpperCase();
87
+ }
88
+ /**
89
+ * Egy jelölt elfogadható-e: nem üres, hossz-limit alatt, NEM denylist-prefix, és még nem láttuk.
90
+ * A prefix = az első `-` előtti szegmens (`BFR-AM-012` → `BFR`; `UTF-8` → `UTF`).
91
+ */
92
+ static isAcceptable(candidate, seen) {
93
+ if (!candidate.length || candidate.length > FAM_ReferenceCode_Util.MAX_CODE_LENGTH || seen.has(candidate)) {
94
+ return false;
95
+ }
96
+ const prefix = candidate.split('-', 1)[0];
97
+ return !FAM_ReferenceCode_Util.PREFIX_DENYLIST.has(prefix);
98
+ }
99
+ /** Egy minta `g`-flagű, friss-state másolata (a megosztott `g`-minta `lastIndex`-e ne szivárogjon). */
100
+ static global(pattern) {
101
+ const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`;
102
+ return new RegExp(pattern.source, flags);
103
+ }
104
+ }
105
+ exports.FAM_ReferenceCode_Util = FAM_ReferenceCode_Util;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_VERSION = void 0;
4
+ const package_json_1 = require("../../package.json");
5
+ /**
6
+ * A FAM package **futás-idejű verziója** — a `package.json`-ból (build-időben a `build/package.json`-ba
7
+ * másolva, a bootstrap is innen olvas). Egyetlen SSOT a verzió-riporthoz: a `/api/health` + a `/status`
8
+ * endpoint, és a `fam serve` boot-után-riport ezt adja vissza (a hardcode-olt `v0.0.1` helyett).
9
+ */
10
+ exports.FAM_VERSION = package_json_1.version;
@@ -27,6 +27,7 @@ exports.famEntryBaseProperties = {
27
27
  embeddingStatus: { type: 'string', index: true, default: 'pending' },
28
28
  scopePath: { type: 'array', default: [] },
29
29
  referenceLinks: { type: 'string[]', default: [] },
30
+ referenceCodes: { type: 'string[]', index: true, default: [] },
30
31
  weight: { type: 'number', default: 1 },
31
32
  importance: { type: 'number', index: true },
32
33
  source: { type: 'object' },
@@ -38,6 +38,12 @@ class FAM_Entry extends fsm_dynamo_1.DyFM_Metadata {
38
38
  scopePath = [];
39
39
  /** Feloldott reference-entitás `_id`-k (alias/fogalom kapcsolat — dsgn-002 §3). */
40
40
  referenceLinks = [];
41
+ /**
42
+ * A tartalomban felismert, normalizált azonosító-kódok (`REQ-AUTH-001`, `FEAT-001`, `BFR-AM-012`, …)
43
+ * — FEAT-003 inverted-index kulcs. A write-path (`embedAndPersist`) tölti a `FAM_ReferenceCode_Util`-lal;
44
+ * a read-path determinisztikus `find({ referenceCodes: $in })`-nel húzza be az ÖSSZES kódot citáló elemet.
45
+ */
46
+ referenceCodes = [];
41
47
  // --- pontozás ---
42
48
  /** Retrieval-score multiplikátor (`> 0`); `0` → kizárt. Default `1.0` (per-tár override). */
43
49
  weight = 1;
@@ -10,7 +10,9 @@ const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
10
10
  * `ingestRunId`-jét (`FAM_Entry.ingestRunId`) — így a run chunkjai csoportosan listázhatók/
11
11
  * törölhetők (dsgn-004 §5). MP-4 a fogyasztó/író; itt a stabil minimál szerződés.
12
12
  *
13
- * `_id` = a `runId` (UUID v4).
13
+ * `_id` = a `runId` — a Dynamo `saveData` által GENERÁLT Mongo ObjectId (stringként), a `FAM_Scope`
14
+ * mintáját követve. (A dsgn-004 §5 eredetileg UUID v4-et írt, de a Dynamo db-rétege `findById`-dal
15
+ * cast-olja a `_id`-t ObjectId-ra, így a client-generált UUID nem használható `_id`-ként.)
14
16
  */
15
17
  class FAM_IngestRun_DataModel extends fsm_dynamo_1.DyFM_Metadata {
16
18
  /** A futtatás triggere. */
@@ -16,6 +16,12 @@ class FAM_Reference_DataModel extends fsm_dynamo_1.DyFM_Metadata {
16
16
  canonicalTerm;
17
17
  /** Aliasok: typo + fonetikus + STT variánsok, amelyek a canonical terminusra mutatnak. */
18
18
  aliasTerms = [];
19
+ /**
20
+ * NEGATÍV aliasok (disambiguáció — „ne keverjünk dolgokat"): azok a tokenek, amiket ez a reference EXPLICIT
21
+ * NEM old fel (veto). Ha a feloldandó token (normalizáltan) egyezik egy negatív-aliasszal, ez a reference
22
+ * KIESIK a kandidátusok közül (exact + fuzzy + vektor egyaránt) → nem keveredik egy hasonló nevű mással.
23
+ */
24
+ negativeAliases = [];
19
25
  /** Mit jelent / mire használt (a `canonicalTerm`-mel együtt adja a vektorizált tartalmat). */
20
26
  description;
21
27
  /** Vektorizált reprezentáció (canonicalTerm + description) — fuzzy match (embedding MP-2). */
@@ -52,6 +58,7 @@ exports.famReference_dataParams = new fsm_dynamo_1.DyFM_DataModel_Params({
52
58
  properties: {
53
59
  canonicalTerm: { type: 'string', index: true, required: true },
54
60
  aliasTerms: { type: 'string[]', index: true, default: [] },
61
+ negativeAliases: { type: 'string[]', index: true, default: [] },
55
62
  description: { type: 'string' },
56
63
  contentVector: {
57
64
  type: 'number[]', vectorizedFrom: ['canonicalTerm', 'description'],
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_CAPTURE_DEFAULT_KIND = exports.FAM_DEFAULT_CAPTURE_SCOPE = void 0;
4
+ /**
5
+ * FEAT-006 — az auto-capture default scope-ja, ha a hívó (hook) nem ad meg explicit scope-ot. A `write`
6
+ * KÖTELEZŐ scope-ot kíván; a hook-vezérelt capture ergonómiájához itt egy dedikált `session/agent-captures`
7
+ * home, ahova a kontextus-nélküli observation-ök esnek. A hívó felülírhatja (pl. projekt-scope-pal).
8
+ */
9
+ exports.FAM_DEFAULT_CAPTURE_SCOPE = [{ layer: 'session', rawName: 'agent-captures' }];
10
+ /** Az auto-capture default `kind`-ja (a `memory`-tár observation-jelölője). */
11
+ exports.FAM_CAPTURE_DEFAULT_KIND = 'observation';
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_AutoCapture_ControlService = void 0;
4
+ const error_codes_const_1 = require("../../../_collections/error-codes.const");
5
+ const fam_error_factory_util_1 = require("../../../_collections/fam-error-factory.util");
6
+ const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
7
+ const embedding_1 = require("../../embedding");
8
+ const ingest_1 = require("../../ingest");
9
+ const fam_mcp_interface_1 = require("../../mcp/_models/interfaces/fam-mcp.interface");
10
+ const fam_write_tool_service_1 = require("../../mcp/_services/fam-write-tool.service");
11
+ const fam_capture_const_1 = require("../_collections/fam-capture.const");
12
+ /**
13
+ * `FAM_AutoCapture_ControlService` (FEAT-006 1. réteg) — a hook-vezérelt **outbound session→`memory` auto-write**
14
+ * a claude-mem-paritás fő rése (a `write` kurált, explicit; ez nagy-frekvenciás, dedup-olt). Singleton.
15
+ *
16
+ * **Content-hash dedup (idempotencia):** a nagy-frekvenciás capture ELŐSZÖR ellenőrzi, van-e MÁR aktív
17
+ * `memory`-bejegyzés azonos kanonikus content-hash-sel (`FAM_ContentHash_Util`, a delta-detection SSOT-ja) —
18
+ * ha igen, **nem keletkezik duplikátum** (`captured:false`, `skipped:'duplicate'`). Egyébként a `write` create-
19
+ * úton ír (agent-provenance + auto-embed); a `contentHash`-t a create pillanatában explicit beírja (robusztus
20
+ * dedup akkor is, ha az embed később hibázna). A scope default-ol (`FAM_DEFAULT_CAPTURE_SCOPE`), ha a hook nem ad.
21
+ *
22
+ * **FIGYELEM (memory: dynts_dataservice_eager_resolve):** NEM tartunk élő DataService mezőt — minden DB-művelet
23
+ * előtt lazy `new FAM_Entry_DataService`.
24
+ */
25
+ class FAM_AutoCapture_ControlService {
26
+ static _instance;
27
+ issuer = 'FAM_AutoCapture_ControlService';
28
+ static getInstance() {
29
+ if (!FAM_AutoCapture_ControlService._instance) {
30
+ FAM_AutoCapture_ControlService._instance = new FAM_AutoCapture_ControlService();
31
+ }
32
+ return FAM_AutoCapture_ControlService._instance;
33
+ }
34
+ /**
35
+ * Egy session-observation auto-capture-je a `memory`-tárba. Content-hash dedup → ha új, `write` create
36
+ * (agent-provenance + auto-embed). Üres `content` → leíró validációs hiba (dsgn-008, nem néma).
37
+ */
38
+ async capture(input) {
39
+ const content = (input?.content ?? '').trim();
40
+ if (!content) {
41
+ throw fam_error_factory_util_1.FAM_Error_Util.create({
42
+ errorCode: error_codes_const_1.FAM_ERROR_CODES.valWriteMissingField,
43
+ message: 'Az `auto_capture` `content`-et igényel (a megfigyelés vektorizálandó szövege).',
44
+ issuer: this.issuer,
45
+ context: { operation: 'auto-capture' },
46
+ });
47
+ }
48
+ const contentHash = ingest_1.FAM_ContentHash_Util.hash(content);
49
+ // Dedup: van-e MÁR aktív memory-bejegyzés azonos content-hash-sel? → idempotens skip (nincs duplikátum).
50
+ const existing = await this.findByHash(contentHash);
51
+ if (existing && existing._id) {
52
+ return { captured: false, skipped: 'duplicate', id: existing._id, contentHash: contentHash };
53
+ }
54
+ // Új capture → a `write` create-úton (agent-provenance, auto-embed). A `source.path` a sessionId
55
+ // provenance-jelölő (a `fields.source` felülírja a write default `{type:'agent'}`-ját).
56
+ const writeInput = {
57
+ table: fam_table_type_enum_1.FAM_Table.memory,
58
+ operation: fam_mcp_interface_1.FAM_WriteOperation.create,
59
+ content: content,
60
+ scopePath: (input.scopePath && input.scopePath.length) ? input.scopePath : fam_capture_const_1.FAM_DEFAULT_CAPTURE_SCOPE,
61
+ tags: input.tags,
62
+ kind: input.kind ?? fam_capture_const_1.FAM_CAPTURE_DEFAULT_KIND,
63
+ fields: input.sessionId
64
+ ? { contentHash: contentHash, source: { type: 'agent', path: input.sessionId } }
65
+ : { contentHash: contentHash },
66
+ };
67
+ const output = await fam_write_tool_service_1.FAM_WriteTool_Service.getInstance().handle(writeInput);
68
+ return {
69
+ captured: true,
70
+ id: output.created[0] ?? null,
71
+ contentHash: contentHash,
72
+ warnings: output.warnings.length ? output.warnings : undefined,
73
+ };
74
+ }
75
+ /** Az első aktív `memory`-bejegyzés az adott content-hash-sel (a dedup kulcsa) — vagy `null`, ha nincs. */
76
+ async findByHash(contentHash) {
77
+ const registryEntry = embedding_1.FAM_StoreRegistry_Util.getEntry(fam_table_type_enum_1.FAM_Table.memory);
78
+ if (!registryEntry) {
79
+ return null;
80
+ }
81
+ const dataService = new embedding_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
82
+ const filter = { contentHash: contentHash };
83
+ const matches = await dataService.findDataList(filter);
84
+ return matches.length ? matches[0] : null;
85
+ }
86
+ }
87
+ exports.FAM_AutoCapture_ControlService = FAM_AutoCapture_ControlService;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_CAPTURE_DEFAULT_KIND = exports.FAM_DEFAULT_CAPTURE_SCOPE = exports.FAM_AutoCapture_ControlService = void 0;
4
+ var fam_auto_capture_control_service_1 = require("./_services/fam-auto-capture.control-service");
5
+ Object.defineProperty(exports, "FAM_AutoCapture_ControlService", { enumerable: true, get: function () { return fam_auto_capture_control_service_1.FAM_AutoCapture_ControlService; } });
6
+ var fam_capture_const_1 = require("./_collections/fam-capture.const");
7
+ Object.defineProperty(exports, "FAM_DEFAULT_CAPTURE_SCOPE", { enumerable: true, get: function () { return fam_capture_const_1.FAM_DEFAULT_CAPTURE_SCOPE; } });
8
+ Object.defineProperty(exports, "FAM_CAPTURE_DEFAULT_KIND", { enumerable: true, get: function () { return fam_capture_const_1.FAM_CAPTURE_DEFAULT_KIND; } });
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_EmbeddingPrefix_Util = void 0;
4
+ /**
5
+ * `FAM_EmbeddingPrefix_Util` (FEAT-004) — **task-prefix + query/document aszimmetria** (pure, állapot-
6
+ * mentes). A live-keresés-teszt (2026-06-20) bizonyította: a FAM a NYERS szöveget embeddeli, de a
7
+ * `nomic-embed-text-v1.5` (és sok instruction-tuned modell) `search_query:` / `search_document:` prefixet
8
+ * VÁR — enélkül magas a zaj-padló (~0.5–0.63 a nem-kapcsolódóknál is) + gyenge a szeparáció. A
9
+ * `FAM_EmbedCallType` MÁR megkülönbözteti a query-t (`embed-query`) a doc-tól (`embed-write`/`-scan`/
10
+ * `-re-embed`) → per-modell, per-irány prefix sokat javít a találat-szeparáción.
11
+ *
12
+ * **Konzisztencia-szerződés:** a write ÉS a read UGYANAZT a sémát alkalmazza (doc-prefix mentéskor,
13
+ * query-prefix kereséskor). Ha egy meglévő korpusz prefix NÉLKÜL készült, a séma BEKAPCSOLÁSA `re-embed`-et
14
+ * igényel (a doc-oldal újraírásához); egy friss korpusz eleve konzisztens.
15
+ *
16
+ * **Mock-provider:** a hívó (`FAM_Embedding_ControlService`) `mock` providernél `off`-ot ad — a mock
17
+ * determinisztikus hash, az aszimmetrikus prefix elrontaná a query↔doc egyezést (a teszt-double match-ét).
18
+ */
19
+ class FAM_EmbeddingPrefix_Util {
20
+ /** A nomic-embed (és OpenAI-kompatibilis instruction-tuned) család sémája. */
21
+ static NOMIC_SCHEME = {
22
+ queryPrefix: 'search_query: ',
23
+ documentPrefix: 'search_document: ',
24
+ };
25
+ /** Modell-név-minta → séma. Bővíthető (új instruction-tuned modell-családokra). */
26
+ static CATALOG = [
27
+ { pattern: /nomic-embed/i, scheme: FAM_EmbeddingPrefix_Util.NOMIC_SCHEME },
28
+ ];
29
+ /** `true`, ha a hívás a QUERY-oldal (read-vektor); minden más callType document-oldal (write/scan/re-embed). */
30
+ static isQuery(callType) {
31
+ return callType === 'embed-query';
32
+ }
33
+ /** A modellhez tartozó prefix-séma (név-match a katalógusból), vagy `null` ha nincs ismert séma. */
34
+ static resolveScheme(modelId) {
35
+ if (typeof modelId !== 'string' || !modelId.length) {
36
+ return null;
37
+ }
38
+ for (const entry of FAM_EmbeddingPrefix_Util.CATALOG) {
39
+ if (entry.pattern.test(modelId)) {
40
+ return entry.scheme;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ /**
46
+ * A konkrét prefix-string egy modellhez + callType-hoz + mode-hoz. `off` → `''`; `auto` → a név-matchelt
47
+ * séma iránya (vagy `''` ha a modell nem ismert prefix-igényűnek); `on` → a matchelt séma VAGY (ha nincs
48
+ * match) a nomic-default — erőltetve. A `''` prefix = nincs változtatás.
49
+ */
50
+ static resolvePrefix(set) {
51
+ if (set.mode === 'off') {
52
+ return '';
53
+ }
54
+ let scheme = FAM_EmbeddingPrefix_Util.resolveScheme(set.modelId);
55
+ if (set.mode === 'on' && !scheme) {
56
+ scheme = FAM_EmbeddingPrefix_Util.NOMIC_SCHEME;
57
+ }
58
+ if (!scheme) {
59
+ return '';
60
+ }
61
+ return FAM_EmbeddingPrefix_Util.isQuery(set.callType) ? scheme.queryPrefix : scheme.documentPrefix;
62
+ }
63
+ /**
64
+ * A prefix alkalmazása MINDEN szövegre (a `texts` sorrendjét megtartva). Üres prefix → a bemeneti
65
+ * tömb VÁLTOZATLANUL (zero-overhead a prefix-mentes esetre — pl. openai/mock/off).
66
+ */
67
+ static apply(set) {
68
+ const prefix = FAM_EmbeddingPrefix_Util.resolvePrefix({
69
+ modelId: set.modelId, callType: set.callType, mode: set.mode,
70
+ });
71
+ if (!prefix) {
72
+ return set.texts;
73
+ }
74
+ return set.texts.map((text) => `${prefix}${text}`);
75
+ }
76
+ }
77
+ exports.FAM_EmbeddingPrefix_Util = FAM_EmbeddingPrefix_Util;
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_DuplicateScan_ControlService = void 0;
4
+ const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
+ const fam_store_registry_const_1 = require("../_collections/fam-store-registry.const");
6
+ const fam_entry_data_service_1 = require("./fam-entry.data-service");
7
+ const fam_vector_search_control_service_1 = require("./fam-vector-search.control-service");
8
+ /**
9
+ * `FAM_DuplicateScan_ControlService` — **near-duplikátum / multiplikáció FELDERÍTŐ eszköz** (user-direktíva
10
+ * 2026-06-21). Egy tár vektor-pool-jában megkeresi a NAGYON HASONLÓ (koszinusz ≥ `threshold`) entry-ket, és
11
+ * **cluster-ekbe** vonja őket (union-find a hasonlóság-éleken). **NEM töröl** — csak megmutatja az
12
+ * ismétlődéseket (dokumentumokban és memóriákban is várható), hogy a user felismerhesse a mintákat és DÖNTSÖN.
13
+ *
14
+ * **Algoritmus + korlátok:** minden (capolt) entry-re a pool top-`neighbors` koszinusz-szomszédja → a küszöb
15
+ * feletti párok élek → union-find → komponensek (méret ≥ 2 = duplikátum-cluster). A `maxEntries` cap védi az
16
+ * O(n²)-blowup-tól (a `capped` flag JELZI, ha nem a teljes tárat néztük — nincs néma csonkolás). Read-only.
17
+ */
18
+ class FAM_DuplicateScan_ControlService {
19
+ static _instance;
20
+ issuer = 'FAM_DuplicateScan_ControlService';
21
+ /** Default koszinusz-küszöb: 0.95 (e felett „nagyon hasonló"). Hangolható a `threshold` inputtal. */
22
+ static DEFAULT_THRESHOLD = 0.95;
23
+ /** Default szomszéd-szám entry-nként (a top-K a duplikátum-élekhez). */
24
+ static DEFAULT_NEIGHBORS = 10;
25
+ /** Default entry-cap (az O(n²) elleni védelem; a `capped` jelzi, ha a tár nagyobb). */
26
+ static DEFAULT_MAX_ENTRIES = 3000;
27
+ /** Default output-cluster-cap (a legnagyobb cluster-ek elöl). */
28
+ static DEFAULT_MAX_CLUSTERS = 100;
29
+ /** A riport-snippet max hossza. */
30
+ static SNIPPET_LENGTH = 140;
31
+ static getInstance() {
32
+ if (!FAM_DuplicateScan_ControlService._instance) {
33
+ FAM_DuplicateScan_ControlService._instance = new FAM_DuplicateScan_ControlService();
34
+ }
35
+ return FAM_DuplicateScan_ControlService._instance;
36
+ }
37
+ /** Egy tár near-duplikátum scan-je (read-only). Üres pool → üres eredmény (nem dob). */
38
+ async scan(input) {
39
+ const threshold = input.threshold ?? FAM_DuplicateScan_ControlService.DEFAULT_THRESHOLD;
40
+ const neighbors = input.neighbors ?? FAM_DuplicateScan_ControlService.DEFAULT_NEIGHBORS;
41
+ const maxEntries = input.maxEntries ?? FAM_DuplicateScan_ControlService.DEFAULT_MAX_ENTRIES;
42
+ const maxClusters = input.maxClusters ?? FAM_DuplicateScan_ControlService.DEFAULT_MAX_CLUSTERS;
43
+ const registryEntry = fam_store_registry_const_1.FAM_StoreRegistry_Util.getEntry(input.table);
44
+ const search = fam_vector_search_control_service_1.FAM_VectorSearch_ControlService.getInstance();
45
+ const totalInPool = registryEntry ? search.getPoolSize(input.table) : 0;
46
+ const empty = {
47
+ table: input.table, threshold: threshold, totalInPool: totalInPool, scanned: 0,
48
+ capped: false, clusterCount: 0, duplicateEntryCount: 0, clusters: [],
49
+ };
50
+ if (!registryEntry || !totalInPool) {
51
+ return empty;
52
+ }
53
+ // 1. A (capolt) (id, vektor) halmaz lean-streamelése (NEM a teljes content — csak a vektorok).
54
+ const dataService = new fam_entry_data_service_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
55
+ const ids = [];
56
+ const vectors = [];
57
+ const filter = { embeddingStatus: 'completed' };
58
+ await dataService.streamHydratableVectors(filter, (id, contentVector) => {
59
+ if (ids.length < maxEntries) {
60
+ ids.push(id);
61
+ vectors.push(contentVector);
62
+ }
63
+ });
64
+ const scanned = ids.length;
65
+ const capped = totalInPool > scanned;
66
+ // 2. Élek: minden entry top-szomszédai a küszöb felett (a saját maga kizárva). Per-entry try/catch
67
+ // (egy dimenzió-/embedding-anomália NE döntse az egész scan-t).
68
+ const edges = [];
69
+ for (let i = 0; i < scanned; i++) {
70
+ try {
71
+ const hits = await search.search({ table: input.table, queryVector: vectors[i], topK: neighbors + 1 });
72
+ for (const hit of hits) {
73
+ if (hit.id !== ids[i] && hit.score >= threshold) {
74
+ edges.push({ a: ids[i], b: hit.id, score: hit.score });
75
+ }
76
+ }
77
+ }
78
+ catch (error) {
79
+ fsm_dynamo_1.DyFM_Log.warn(`[FAM dup-scan] '${input.table}' entry '${ids[i]}' keresése kihagyva: ${error?.message}`);
80
+ }
81
+ }
82
+ // 3. Union-find → komponensek (méret ≥ 2 = duplikátum-cluster). Tiszta.
83
+ const components = FAM_DuplicateScan_ControlService.clusterFromEdges(edges);
84
+ // 4. Snippet-ek CSAK a clustered id-kre (kis halmaz; a felismeréshez).
85
+ const clusteredIds = new Set();
86
+ for (const component of components) {
87
+ for (const id of component.ids) {
88
+ clusteredIds.add(id);
89
+ }
90
+ }
91
+ const metaById = await this.fetchMeta(dataService, clusteredIds);
92
+ // 5. Cluster-ek építése + rendezés (legnagyobb/legtömörebb elöl) + output-cap.
93
+ const clusters = components
94
+ .map((component) => ({
95
+ size: component.ids.length,
96
+ maxScore: component.maxScore,
97
+ minScore: component.minScore,
98
+ members: component.ids.map((id) => ({
99
+ id: id,
100
+ kind: metaById.get(id)?.kind,
101
+ snippet: metaById.get(id)?.snippet,
102
+ })),
103
+ }))
104
+ .sort((left, right) => right.size - left.size || right.maxScore - left.maxScore);
105
+ const duplicateEntryCount = clusters.reduce((sum, cluster) => sum + cluster.size, 0);
106
+ return {
107
+ table: input.table,
108
+ threshold: threshold,
109
+ totalInPool: totalInPool,
110
+ scanned: scanned,
111
+ capped: capped,
112
+ clusterCount: clusters.length,
113
+ duplicateEntryCount: duplicateEntryCount,
114
+ clusters: clusters.slice(0, maxClusters),
115
+ };
116
+ }
117
+ /**
118
+ * **Tiszta union-find**: a hasonlóság-élekből összefüggő komponensek (méret ≥ 2). Minden komponens id-i
119
+ * rendezve + a komponens score-tartománya (min/max él-score). Determinista (azonos input → azonos output).
120
+ * Side-effect-mentes → unit-tesztelhető a pool/DB nélkül.
121
+ */
122
+ static clusterFromEdges(edges) {
123
+ const parent = new Map();
124
+ const ensure = (node) => {
125
+ if (!parent.has(node)) {
126
+ parent.set(node, node);
127
+ }
128
+ };
129
+ const find = (node) => {
130
+ let root = node;
131
+ while (parent.get(root) !== root) {
132
+ root = parent.get(root);
133
+ }
134
+ // path-compression
135
+ let cursor = node;
136
+ while (parent.get(cursor) !== root) {
137
+ const next = parent.get(cursor);
138
+ parent.set(cursor, root);
139
+ cursor = next;
140
+ }
141
+ return root;
142
+ };
143
+ for (const edge of edges) {
144
+ ensure(edge.a);
145
+ ensure(edge.b);
146
+ const rootA = find(edge.a);
147
+ const rootB = find(edge.b);
148
+ if (rootA !== rootB) {
149
+ parent.set(rootA, rootB);
150
+ }
151
+ }
152
+ const idsByRoot = new Map();
153
+ for (const node of parent.keys()) {
154
+ const root = find(node);
155
+ const bucket = idsByRoot.get(root);
156
+ if (bucket) {
157
+ bucket.add(node);
158
+ }
159
+ else {
160
+ idsByRoot.set(root, new Set([node]));
161
+ }
162
+ }
163
+ const scoreByRoot = new Map();
164
+ for (const edge of edges) {
165
+ const root = find(edge.a);
166
+ const current = scoreByRoot.get(root) ?? { min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY };
167
+ current.min = Math.min(current.min, edge.score);
168
+ current.max = Math.max(current.max, edge.score);
169
+ scoreByRoot.set(root, current);
170
+ }
171
+ const result = [];
172
+ for (const [root, ids] of idsByRoot) {
173
+ if (ids.size < 2) {
174
+ continue;
175
+ }
176
+ const score = scoreByRoot.get(root) ?? { min: 0, max: 0 };
177
+ result.push({ ids: [...ids].sort(), maxScore: score.max, minScore: score.min });
178
+ }
179
+ return result;
180
+ }
181
+ /** A clustered id-k jelleg + snippet metaadata (egy lean lekérdezés, csak a kis clustered halmazra). */
182
+ async fetchMeta(dataService, clusteredIds) {
183
+ const metaById = new Map();
184
+ if (!clusteredIds.size) {
185
+ return metaById;
186
+ }
187
+ const entries = await dataService.findHydratableList({ _id: { $in: [...clusteredIds] } });
188
+ for (const entry of entries) {
189
+ const id = String(entry._id ?? '');
190
+ if (!id) {
191
+ continue;
192
+ }
193
+ const content = typeof entry.content === 'string' ? entry.content : '';
194
+ metaById.set(id, {
195
+ kind: entry.kind,
196
+ snippet: content.replace(/\s+/g, ' ').trim().slice(0, FAM_DuplicateScan_ControlService.SNIPPET_LENGTH),
197
+ });
198
+ }
199
+ return metaById;
200
+ }
201
+ }
202
+ exports.FAM_DuplicateScan_ControlService = FAM_DuplicateScan_ControlService;
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_EmbeddingPipeline_ControlService = void 0;
4
- const tslib_1 = require("tslib");
5
- const crypto = tslib_1.__importStar(require("crypto"));
6
4
  const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
7
5
  const error_codes_const_1 = require("../../../_collections/error-codes.const");
6
+ const fam_reference_code_util_1 = require("../../../_collections/fam-reference-code.util");
8
7
  const errors_control_service_1 = require("../../../_routes/server/errors/errors.control-service");
9
8
  const fam_store_registry_const_1 = require("../_collections/fam-store-registry.const");
9
+ const fam_content_hash_util_1 = require("../../ingest/_collections/fam-content-hash.util");
10
10
  const fam_embedding_control_service_1 = require("./fam-embedding.control-service");
11
11
  const fam_entry_data_service_1 = require("./fam-entry.data-service");
12
12
  const fam_vector_search_control_service_1 = require("./fam-vector-search.control-service");
@@ -55,8 +55,15 @@ class FAM_EmbeddingPipeline_ControlService {
55
55
  return { status: 'error', dims: 0 };
56
56
  }
57
57
  const dataService = new fam_entry_data_service_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
58
+ // FEAT-003: a tartalomban felismert azonosító-kódok inverted-index-be írása — az embed-sikertől
59
+ // FÜGGETLENÜL (az embed-try ELŐTT), hogy a kód-citáció a még nem embeddelt / embed-bukott elemen is
60
+ // megjelenjen a determinisztikus read-expanzióban. A re-embed (teljes tár) így BACKFILL-eli a régieket.
61
+ await this.persistReferenceCodes(set.entry, dataService);
58
62
  const composed = this.composeVectorizedText(set.entry, registryEntry.vectorizedFrom);
59
- const contentHash = this.sha256(composed);
63
+ // A `contentHash` a KANONIKUS content-hash (`FAM_ContentHash_Util`, a delta-detection SSOT-ja) — NEM
64
+ // a `composed` vektor-szöveg hash-e. Korábban az utóbbit írtuk a `contentHash` mezőbe, amit a delta
65
+ // a `chunk.content` hash-éhez hasonlít → SOHA nem egyezett → minden re-scan `modified` (fölös re-embed).
66
+ const contentHash = fam_content_hash_util_1.FAM_ContentHash_Util.hash(typeof set.entry.content === 'string' ? set.entry.content : '');
60
67
  if (!composed.trim().length) {
61
68
  // Nincs vektorizálható content → marad pending (a write-validáció a content-kötelezőséget
62
69
  // külön kezeli; itt csak jelezzük, hogy nincs mit embeddelni).
@@ -181,13 +188,30 @@ class FAM_EmbeddingPipeline_ControlService {
181
188
  }
182
189
  return parts.join('\n');
183
190
  }
184
- /** SHA-256(normalizált szöveg) — a delta-detection contentHash-e (dsgn-001 / dsgn-004 §4). */
185
- sha256(text) {
186
- return crypto.createHash('sha256').update(this.normalize(text)).digest('hex');
191
+ /**
192
+ * FEAT-003 — a `content`-ből felismert azonosító-kódok `referenceCodes`-ba írása (`FAM_ReferenceCode_Util`).
193
+ * **Changed-only:** ha a felismert halmaz megegyezik a már perzistálttal → NINCS `$set` (a teljes-tár
194
+ * re-embed N entry-jén a fölös write elkerülése). Az in-memory `entry.referenceCodes`-t is frissíti (a
195
+ * hívó esetleges azonnali olvasásához). DB-`_id` nélkül no-op.
196
+ */
197
+ async persistReferenceCodes(entry, dataService) {
198
+ if (!entry._id) {
199
+ return;
200
+ }
201
+ const codes = fam_reference_code_util_1.FAM_ReferenceCode_Util.extract(typeof entry.content === 'string' ? entry.content : '');
202
+ if (this.sameCodeSet(codes, entry.referenceCodes ?? [])) {
203
+ return;
204
+ }
205
+ entry.referenceCodes = codes;
206
+ await dataService.setReferenceCodes(entry._id, codes);
187
207
  }
188
- /** Egyszerű normalizálás a hash-hez (whitespace-tömörítés + trim) — a delta-konzisztenciához. */
189
- normalize(text) {
190
- return text.replace(/\s+/g, ' ').trim();
208
+ /** Két kód-halmaz egyezése (sorrend-független) — a changed-only `$set`-kapuhoz. */
209
+ sameCodeSet(a, b) {
210
+ if (a.length !== b.length) {
211
+ return false;
212
+ }
213
+ const setB = new Set(b);
214
+ return a.every((code) => setB.has(code));
191
215
  }
192
216
  /** A scope-szűkítő filter (a `scopePath` LEVÉL scopeId-jára). Üres scope → teljes tár (`!_deleted`). */
193
217
  buildScopeFilter(scopePath) {